<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Anton’s Substack]]></title><description><![CDATA[My personal Substack (and that's true!) about iOS Development]]></description><link>https://antongubarenko.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!mzA2!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1eb672de-f6cd-47a2-98b2-6de3f5be6ffe_2316x3088.jpeg</url><title>Anton’s Substack</title><link>https://antongubarenko.substack.com</link></image><generator>Substack</generator><lastBuildDate>Tue, 10 Mar 2026 13:08:14 GMT</lastBuildDate><atom:link href="https://antongubarenko.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Anton Gubarenko]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[antongubarenko@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[antongubarenko@substack.com]]></itunes:email><itunes:name><![CDATA[Anton Gubarenko]]></itunes:name></itunes:owner><itunes:author><![CDATA[Anton Gubarenko]]></itunes:author><googleplay:owner><![CDATA[antongubarenko@substack.com]]></googleplay:owner><googleplay:email><![CDATA[antongubarenko@substack.com]]></googleplay:email><googleplay:author><![CDATA[Anton Gubarenko]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Fortify Your App: Essential Strategies to Strengthen Security Q&A]]></title><description><![CDATA[Answers from Apple Engineers about security]]></description><link>https://antongubarenko.substack.com/p/fortify-your-app-essential-strategies</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/fortify-your-app-essential-strategies</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Mon, 09 Mar 2026 07:05:14 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/4c9c618f-f3de-45e9-a9b8-10c4074845af_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Meetups and webinars continue, and this time Apple hosted a session on <strong><a href="https://developer.apple.com/videos/play/meet-with-apple/265/">Security Strategies</a></strong>, covering topics such as Enhanced Security, Memory Integrity, and writing secure code with Swift. Previously, I was already surprised by a four-hour duration. Now it&#8217;s almost six hours (six!) with three breaks, and it requires a lot of focus.</p><blockquote><p>In the survey after the session, I mentioned that splitting it into separate sections would be better, in my opinion. Finding that much time during either the night or regular working hours is not always possible. Let&#8217;s hope they are actually reading the feedback.</p></blockquote><ul><li><p><a href="https://developer.apple.com/videos/play/meet-with-apple/265/">Apple Video source</a></p></li><li><p><a href="https://www.youtube.com/watch?v=UZeSyodAszc">YouTube version</a></p></li></ul><p>As usual, questions are splitted into more or less common section. Grammar and punctuation are kept too.</p><div><hr></div><h2>&#128272; Security &amp; Memory Safety</h2><h3>What strategies can we use to evaluate third-party libraries for security vulnerabilities?</h3><p>Hi! Evaluating third-party libraries can be tricky and require a lot of domain expertise as there are a wide variety of issues to look for. This is especially true if you only have access to the libraries in a binary form. Certain features&#8212;like EMTE&#8212;serve as mitigations and can be applied (in some cases) without a recompile, which can help in cases where you cannot easily audit the source. When auditing the source, some good hints for finding security relevant bugs are: 1. Memory unsafe code (C/C++, Swift could which uses unsafe, etc.), especially decoders and deserialization routines (e.g. bespoke file/request format parsing). 2. File path construction and archive expansion (zip files, etc.) are a common source of problems due to path traversal 3. Whether or not the code is memory safe, take special care to understand where it comes from and whether you trust it.</p><div><hr></div><h3>What are the potential security vulnerabilities associated with storing data in <code>UserDefaults</code> and plist files, and what best practices should be followed to protect against tampering or exposure?</h3><p>In terms of pure data security, both approaches are fairly similar. The data is being stored in the file system and is protected the same way any other file does. However, using your own plist file does mean you can directly set the file&#8217;s protection level, which is another reason to avoid storing sensitive data in <code>UserDefaults</code>. Similarly, both are using the same on-disk format and data parser. Theoretically, that&#8217;s an attack vector just like any file parser. However, this particular parser is relatively simple compared to other formats (for example, SQL) and so widely used throughout the entire system that I&#8217;d consider it quite safe.</p><div><hr></div><h3>For a standalone iOS app with no backend, what&#8217;s Apple&#8217;s recommended baseline for protecting sensitive user-entered data at rest? Is &#8216;Complete Protection&#8217; (<code>NSFileProtectionComplete</code>) plus Keychain for secrets the correct default?</h3><p>The baseline for most data should be <code>NSFileProtectionCompleteUntilFirstUserAuthentication</code> (for files) and <code>kSecAttrAccessibleAfterFirstUnlock</code> (keychain). The issue with storing data at higher security levels is that data stored at &#8220;Complete&#8221; can really only be safely accessed while your app is in the foreground. More specifically, the data will only be accessible if the device is unlocked, but that state changes independently from background execution, meaning data can suddenly become inaccessible anytime your app is running in the background. You can and should use &#8220;Complete&#8221; whenever you can, but it&#8217;s a choice that needs to be made as part of your app&#8217;s overall design, not as an automatic default.</p><div><hr></div><h3>Is it safe to use the Data type&#8217;s <code>withUnsafeBytes</code> method? I find myself using it a lot when parsing values from Bluetooth devices.</h3><p>Methods with &#8220;unsafe&#8221; in the name can be used safely, but it requires more care and attention to do so. You won&#8217;t always be able to avoid using such methods, but you can carefully isolate such code and of course make sure to devote more effort to reviewing and verifying any code that uses unsafe methods or types.</p><div><hr></div><h3>I&#8217;m confused of the purpose of blast door, given the validation of the image was ultimately down to messages&#8230; surely blast door would protect and / or sanitise the object/data recieved? Or have I missed the point?</h3><p>Properly validating images can itself be a perilous task and so it&#8217;s one best not performed inside a (relatively) privileged process like Messages. BlastDoor validates and unpacks attachments in a highly restricted environment and then passes a much safer, simpler version of it to Messages. So, for example, Messages may give BlastDoor a JPG and BlastDoor will ultimately hand back a simple bitmap representation of it. This indemnifies Messages against memory corruption bugs in the JPG parser.</p><div><hr></div><h3>Apple on messages for example never denies uploads of different file types, but then how does it protect against file-based vulnerabilities?</h3><p>Messages allows sending (mostly) arbitrary files but&#8212;notably&#8212;it does not parse/preview/unpack arbitrary files. Messages and BlastDoor work together to ensure that attachments which are processed are processed in a way that&#8217;s safe. It is generally safe to accept arbitrary files so long as they are not processed.</p><div><hr></div><h3>Memory safe languages like Swift or Rust have sometimes to bypass security checks (e.g. to access the kernel). Which protections and tools can be used in that case?</h3><p>On Apple platforms, EMTE is a great way to mitigate issues caused by this gap. Even kernel accesses to user memory are tag checked, and so out-of-bounds/use-after-free accesses to user memory will still be reported according to the process&#8217; enforcement mode. Other techniques such as fuzzing with libfuzzer and either ASan or EMTE on can also serve as strategies to gain confidence that unsafe code has the desired memory safety properties.</p><div><hr></div><h3>This question may be covered today, but I&#8217;m curious what resources and guidance exists for maximizing security in apps when we build across platforms. SwiftUI is write once, tweak, deploy. How about security? Sandboxing is different as an example on Mac vs iPhone.</h3><p>Hi! Thanks for joining. The event today covers many topics about security and memory safety. Most of the guidance is platform agnostic, and some are programming language agnostic. For additional resources specific to your app and project environments, please refer to Apple Platform Security: <a href="https://support.apple.com/guide/security/welcome/web">https://support.apple.com/guide/security/welcome/web</a></p><div><hr></div><h3>What techniques help verify if a third-party app has adopted security enhancements, hardware memory tagging, security extensions?</h3><p>If you have the application bundle available to you, you can use the codesign tool to view an application&#8217;s entitlements: <code>codesign -d --entitlements - /path/to/application/binary</code> As EMTE is controlled by entitlement, you can use this technique to see if EMTE is enabled for a given executable in the app.</p><div><hr></div><h3>What additional features (settings) required for fully Swift apps? is Enhanced Memory safety required (new Capability + config)? How much of security guarantees Swift provides? anything in the talk required to be enabled or is enabled by default?</h3><p>Enabling enhanced security features like PAC, EMTE, and typed allocations is still useful in Swift apps. In certain language modes, Swift apps which do not use <code>unsafe</code> can still have memory corruption issues due to data races (concurrently modifying a reference can cause reference counting errors). Similarly, although your application may be fully safe Swift, it may interact with libraries (provided by Apple or third parties) which are not fully memory safe, and so turning on enhanced security features will help protect you against issues not caused by your code.</p><div><hr></div><h2>&#128737;&#65039; Enhanced Security &amp; Capabilities</h2><h3>For the Pointer Authentication feature, supporting arm64e is required. I understand this applies to third party SDKs, but how about an app that is completely modularized (with all modules in the same workspace), do we need to configure this per module or only the main app target?</h3><p>arm64e is indeed required, and every target that contributes binary code that&#8217;s linked or dynamically loaded into an app does need to have arm64e added as an architecture. When enabling the Enhanced Security capability, Xcode adds the <code>ENABLE_POINTER_AUTHENTICATION</code> build setting (that adds arm64e) as needed, but you may need to add that separately as well.</p><div><hr></div><h3>Is there a <code>cSettings</code> SPM construct to enable PAC, MTE (and other features that do not require the final top-level signing/capabilities entitlement) in a Swift package that has C/C++ targets, so that compiled libraries can be included in an Xcode project using these enhanced security capabilities?</h3><p>Any clang compiler option can be included in cSettings. Caveat: Prior to Swift 6.2, only some clang options were allowed, but starting with Swift 6.2, you can put any option.</p><div><hr></div><h3>Does PAC specifically (enhanced security include PAC and more) work for iOS 14+ deployment targets? You can enable each part of enhanced security individually, so like we know that typed memory allocator compiles nicely for iOS16 or 17+, but we can get PAC compiled for iOS 14+?</h3><p>PAC is tied to the arm64e architecture. arm64e is first supported in iOS 17.4, and generally supported starting with iOS 26 and macOS 26. Universal binaries can be built for arm64e + arm64, and arm64 will be used when arm64e isn&#8217;t supported. When building the universal binary, both architectures can be compiled for an older deployment target, but keep in mind that arm64e will only be used on newer iOS.</p><div><hr></div><h3>Do these features require Xcode 26.4 to debug, run and test with? I&#8217;ve noticed the change in entitlements mentioned in the Xcode 26.4 beta release notes, does that mean that you need to test and run on Apple 26.4 OSes to try out Enhanced Security capabilities now?</h3><p>Most of the capabilities of Enhanced Security are supported on and can be tested on Apple OS versions starting from 26.0. The &#8220;hard mode&#8221; of MTE (under which tag-check violations result in an immediate crash) is only supported beginning in 26.4, so to test with that capability on hardware with MTE support you&#8217;ll need to test with 26.4 or later OS versions. (described in 26.4 release notes [1]) The new enhanced-security-version-string and platform-restrictions-string string version entitlements you noted described in the Xcode 26.4 release notes [2] are set automatically by Xcode 26.4, but can be set manually in your entitlements plists using a text editor if you need to stay on an earlier Xcode version. <br>[1] <a href="https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-26_4-release-notes">https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-26_4-release-notes</a> [2] <a href="https://developer.apple.com/documentation/xcode-release-notes/xcode-26_4-release-notes">https://developer.apple.com/documentation/xcode-release-notes/xcode-26_4-release-notes</a></p><div><hr></div><h3>I&#8217;m having trouble finding the advanced security&#8230; is it under signing in capabilities or is it under the settings of Xcode? Also. Can you build an outlet already contained like I was building a journal app I don&#8217;t want it to be connected to any network services by default or any services</h3><p>Hi Elijah, please refer to the documentation below for steps to enable the Enhanced Security capability: <a href="https://developer.apple.com/documentation/xcode/enabling-enhanced-security-for-your-app">https://developer.apple.com/documentation/xcode/enabling-enhanced-security-for-your-app</a></p><div><hr></div><h3>I&#8217;ve noticed that when enabling MTE, my app keeps crashing on launch with reports pointing to 3rd party SDKs. Whenever I take out one SDK, it just crashes in the next. It seems the reported tag is always 0, which makes me believe it&#8217;s not just violations but maybe a configuration problem on my end?</h3><p>Additionally, arm64 binaries produced by older versions of clang may have issues where the tag is incorrectly stripped from the pointer. Recompiling the binary with a recent compiler should remediate the issue.</p><div><hr></div><h3>Will enabling enhanced security extension from code signing help to find bugs for debug builds on simulators as well?</h3><p>Yes, starting in the 26.4 OS versions, applications that enable MTE (checked-allocations) as part of Enhanced Security will run with MTE enabled in the simulator when running on macOS hardware that supports MTE.</p><div><hr></div><h3>Is enabling standard library hardening recommended for all build types not just for debug? Will it cause any latency issues when enabled for prod configuration? Does <code>$(__LIBRARY_HARDENING_DEFAULT_VALUE)</code> mean no hardening set for project?</h3><p>Yes, we recommend enabling at least the fast hardening mode, even in production builds. It has been designed to have minimal performance impact, but if you have very performance sensitive workloads, as always: benchmark before and after. For configurations with optimization disabled (level set to 0 &#8212; normally Debug configs), <code>__LIBRARY_HARDENING_DEFAULT_VALUE</code> defaults to &#8220;debug&#8221; (the more extensive checks). For optimized configurations (e.g. Release), <code>__LIBRARY_HARDENING_DEFAULT_VALUE</code> defaults to &#8220;fast&#8221; if Enhanced Security is enabled, &#8220;none&#8221; otherwise.</p><div><hr></div><h2>&#128269; Hardware Support &amp; Compatibility</h2><h3>Which of the devices announced this week support memory <a href="https://security.apple.com/blog/memory-integrity-enforcement/">integrity enforcement</a>? </h3><p>Memory Integrity Enforcement is supported on A19, A19 Pro, M5, M5 Pro, and M5 Max, which power iPhone 17e, the new MacBook Air (M5), and the new MacBook Pro (M5 Pro or M5 Max).</p><div><hr></div><h3>Is the memory tag exposed in memgraphs or Instruments in any way?</h3><p>When running under the Allocations template in Instruments, the memory tags do show up in object addresses. In memgraphs and the CLI tools like heap, these tags aren&#8217;t currently exposed, with the addresses correlating directly to their containing VM regions. If you&#8217;re interested to see which regions have tagging enabled, this is available with <code>vmmap --attributes</code></p><div><hr></div><h3>Does Memory Integrity Enforcement support pre-Swift 6.0?</h3><p>Yes, Memory Integrity Enforcement can be used with any Swift version.</p><div><hr></div><h2>&#128260; C/C++ Interoperability</h2><h3>In some C/C++ functions the Swift interop uses unsafe pointers to bytes. Is there something like a <code>Span</code> API over the bytes of a Swift <code>var</code> to ensure a memory-safe and bounds-enforced buffer pointer to pass through to C/C++ code instead of using unsafe methods such as <code>withUnsafeMutableBytes()</code>?</h3><p>Great question! Automatically generated wrapper functions that safely unwrap Span types and pass along the pointer to C/C++ is a feature available since Xcode 26 when the experimental feature SafeInteropWrappers is enabled. This requires annotating std::span parameters with __noescape, or pointer parameters with both __noescape and __counted_by/__sized_by, directly in the header or using API notes. Note that this is only safe if Swift can accurately track the lifetime of the unwrapped pointer, which is why the Span wrapper is not generated without the __noescape annotation. More resources are available at <a href="https://developer.apple.com/videos/play/wwdc2025/311/">https://developer.apple.com/videos/play/wwdc2025/311/</a> and <a href="https://www.swift.org/documentation/cxx-interop/safe-interop/">https://www.swift.org/documentation/cxx-interop/safe-interop/</a>. Since this is an experimental feature with ongoing development, questions and feedback on https://forums.swift.org are extra welcome to help us shape and stabilize this feature!</p><div><hr></div><h2>&#128137; Pointer Authentication</h2><h3>How does PAC work with ObjC method swizzling at runtime?</h3><p>When you use the functions provided by the ObjC runtime, they ensure that any necessary pointer signing is correctly handled.</p><div><hr></div><h3>How does pointer authentication different from other memory defense ways mentioned as part of whole app protection?</h3><p>Pointer authentication makes it more difficult to create a pointer (from an integer) or to modify an existing pointer. This complements technologies such as MTE (which can catch many bound and lifetime errors) and typed allocation (which mitigates the effects of memory re-use).</p><div><hr></div><h3>Why is Pointer Authentication a compile-time, opt-in feature, and not a platform-enforced, runtime-enabled feature? I imagine something like that should be possible if something as magical as Rosetta is also possible &#128578;</h3><p>Additionally, PAC is a compile time change as it requires different instructions throughout the program.</p><div><hr></div><h3>Where are the cryptographic tags used for pointer authentication stored on Apple devices? Are they kept in the Secure Enclave or in another hardware component?</h3><p>The signatures are, however, stored in the upper bits of the pointer itself.</p><div><hr></div><h2>&#128230; Allocators &amp; Memory Management</h2><h3>Are type and alignment independent for typed allocators?</h3><p>Mostly, yes, but not entirely. The typed allocators segregate and isolate allocations by size class and, within each size class, by type space partition. Let&#8217;s call the (size class, type space partition) combination the &#8220;type bucket&#8221; that serves a particular allocation. Requesting aligned allocations (e.g. via <code>aligned_alloc()</code>) can change the effective size class of an allocation because of implementation details of the allocators, and so can change the type bucket that the allocation is served from.</p><div><hr></div><h3>What is best approach to use system allocator for third party SDK e.g. Braze if enabled enhanced security extension for app and memory corruption is noticed?</h3><p>Third-party SDKs linked in to your app/program will generally be using the system allocator automatically and benefit from Memory Integrity Enforcement automatically. If there are memory corruption bugs in those SDKs that Memory Integrity Enforcement features like MTE detect and turn into crashes, you would want to work with the developers of those SDKs to have them fix the underlying bugs. You could use MTE soft mode [1] to avoid having those memory corruptions crash your app while you wait for fixes from the developers, at the cost of the relative reduction in security that that entails. <a href="https://developer.apple.com/documentation/BundleResources/Entitlements/com.apple.security.hardened-process.checked-allocations.soft-mode">https://developer.apple.com/documentation/BundleResources/Entitlements/com.apple.security.hardened-process.checked-allocations.soft-mode</a></p><div><hr></div><h2>&#128207; Bounds Safety</h2><h3>How do bounds safety checks prevent out-of-bounds (OOB) accesses in practice? Is this implemented as a built-in feature in Clang? When developers add annotations, what mechanisms are applied internally to enforce or adjust the bounds checks?</h3><p>Yes, this is built into Clang. With -fbounds-safety enabled Clang will emit bounds checks wherever pointers are dereferenced or reassigned (exception: assigning to <code>__bidi_indexable</code> does not trigger a bounds check, since <code>__bidi_indexable</code> can track the fact that the pointer is out of bounds and defer the bounds check). If the bounds check fails the program will jump to an instruction that traps the process. Clang uses a combination of static analysis and runtime checks to enforce that pointer bounds are respected.</p><div><hr></div><h3>Why is CoreFoundation missing all bounds-checking annotations? Do I have to use <code>__unsafe_forge_single</code> for all initializers?</h3><p>Yes, that is the recommended approach when interoperating with libraries that do not have bounds annotations, when you want to be explicit about the fact that you&#8217;re interacting with unsafe code. This makes it easy to grep for &#8220;unsafe&#8221; in your code base when doing a security audit. If you are confident that the API adheres to a bounds safe interface but simply lacks the annotations, you can redeclare the signature in your local header with added bounds annotations, like this: </p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;ec37d596-eccb-4274-a500-599d87604af2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">//--- system_header.h bar_t * /* 
implicitly __unsafe_indexable */ foo(); 
//--- project_header.h 
#include #include bar_t * __single foo();</code></pre></div><div><hr></div><h2>&#128241; App Store &amp; Privacy</h2><h3>Are there common App Store review pitfalls for apps that store sensitive contact data locally (even if it&#8217;s not medical)? Anything you recommend we avoid to prevent rejection or privacy concerns?</h3><p>Hi Gloria! Please see the following pages on user privacy and data use: </p><p>- <a href="https://developer.apple.com/app-store/user-privacy-and-data-use/">https://developer.apple.com/app-store/user-privacy-and-data-use</a><br>- <a href="https://developer.apple.com/app-store/app-privacy-details/">https://developer.apple.com/app-store/app-privacy-details/</a> <br>- <a href="https://developer.apple.com/documentation/uikit/protecting-the-user-s-privacy/">https://developer.apple.com/documentation/uikit/protecting-the-user-s-privacy/</a> <br>- <a href="https://developer.apple.com/documentation/uikit/requesting-access-to-protected-resources">https://developer.apple.com/documentation/uikit/requesting-access-to-protected-resources</a> <br>- <a href="https://developer.apple.com/documentation/uikit/encrypting-your-app-s-files">https://developer.apple.com/documentation/uikit/encrypting-your-app-s-files</a></p><div><hr></div><h2><strong>&#127942; </strong>Acknowledgments</h2><p>A huge thank-you to everyone who joined in and shared thoughtful, insightful, and engaging questions throughout the session &#8212; your curiosity and input made the discussion truly rich and collaborative.</p><p><strong>Special thanks to:</strong></p><p>Larry Wang, Chandrachud Patil, Christopher Sheats, Gloria Glazebrook, Patrick Hoekstra, Chris CL, Eric Dorphy, AAron Wangugi, Quinten Johnson, Nicholas Levin, Kim Ahlberg, Jason Brooks, Patrick Cousot, Alex Infanti, Ilia, nikolay dubina, Danylo, Pablo Butron, Elijah Cody Bain Black, Paul Floyd, Kim Kyoungsu, Zaid Al-Timimi, Rupinder, Tanner Bennett, Sanket P, Steven Joubanian and Gordon Leete.</p><p>Finally, a heartfelt thank-you to the <strong>Apple team and moderators</strong> for leading session, sharing expert guidance, and offering such clear explanations of the apps optimization techniques. Your contributions made this session an outstanding learning experience for everyone involved.</p><div><hr></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[SwiftUI Foundations: Build Great Apps with SwiftUI Q&A Promo]]></title><description><![CDATA[Answers from Apple Engineers]]></description><link>https://antongubarenko.substack.com/p/swiftui-foundations-build-great-apps-163</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swiftui-foundations-build-great-apps-163</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Tue, 17 Feb 2026 07:04:17 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/97215df7-e012-41c7-8eda-11959a276e3b_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently Apple returned with webinars format where engineers answered questions directly from developers while hosts were talking about different parts of some topic. </p><p>This time <a href="https://developer.apple.com/videos/play/meet-with-apple/267/">it was SwiftUI</a>: from basics to state behavior. If the last session about &#8220;Coding Agents in Xcode&#8220; went for 16 mins and had 3 question - this one is totally different. It took more than 3 hours (!) and more than a 150 question. </p><blockquote><p>But! Questions were not always linked to the session topic. You can find info about Concurrency or SwiftData also.</p></blockquote><p>Even formatting and session questions gathering took hours for me. So, this is truly unique content. There even was a question regarding where can I found the Q&amp;A session transcript later. The answer is: nowhere except here) </p><p>This <strong>huge amount</strong> with a threads (this also were not so common previously) is taking out-of-email size. Follow the link below for the full list of answers:<br><a href="https://antongubarenko.substack.com/p/swiftui-foundations-build-great-apps?r=21t43r&amp;utm_campaign=post&amp;utm_medium=web&amp;triedRedirect=true">SwiftUI Foundations: Build Great Apps with SwiftUI Q&amp;A</a></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[SwiftUI Foundations: Build Great Apps with SwiftUI Q&A]]></title><description><![CDATA[Answers from Apple Engineers]]></description><link>https://antongubarenko.substack.com/p/swiftui-foundations-build-great-apps</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swiftui-foundations-build-great-apps</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Tue, 17 Feb 2026 06:58:41 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/2133454a-be6c-4464-bb74-37f19628e325_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently, Apple returned to the webinar format, where engineers answer developers&#8217; questions directly while hosts cover different parts of a topic.</p><p>This time it was <strong><a href="https://developer.apple.com/go/?id=swiftui-foundations-feb-10">SwiftUI foundations: Build great apps with SwiftUI</a></strong>. If the previous session about &#8220;Coding Agents in Xcode&#8221; lasted 16 minutes and had only 3 questions, this one was completely different. It ran for more than 3 hours (!) and included over 100 questions.</p><blockquote><p>But! The questions were not always tied strictly to the session topic. You&#8217;ll also find discussions about Concurrency and SwiftData.</p></blockquote><p>Even formatting and gathering the session questions took hours for me, so this is truly unique content. Grammar and punctuation were slightly adjusted, but the authors&#8217; formatting was kept as is.</p><p>And without further ado &#8212; here&#8217;s the session Q&amp;A.</p><div><hr></div><h2><strong>&#128190;</strong> SwiftData &amp; Backend</h2><h3>Do you think it would be possible to write a ModelContainer targeting a backend other than iCloud, such as Firebase, and still be compatible with SwiftData framework annotations?</h3><p>This is such an awesome question, the answer personally is I do not know, but would love to try to see if can be done, I think it is possible to write a ModelContainer targeting a backend other than SwiftData, however Firebase is a 3rd party framework so we can&#8217;t provide compatibility with SwiftData framework. SwiftData provides a way to abstract the backend and focus on defining models and relationships.</p><p>Start by defining your SwiftData models with the necessary annotations to specify relationships and attributes. Configure the ModelContainer to use your chosen backend.</p><p>It sounds like you can try to make it work In your application, configure the ModelContainer to use Firebase as the persistent store but then will not be store in SwiftData. You can now perform CRUD operations on your models using SwiftData, leveraging the backend of your choice, but I do not work if that can work. I would love to see that working, personally.</p><p>Implement synchronization and conflict resolution mechanisms between SwiftData and your backend sounds like a hard project. This may require additional configuration or logic tailored to your use case. While challenging, I believe the customization and migration could way too complex. Migration is the key word and being able to work in the future versions.</p><div><hr></div><h3>Does SwiftData support data virtualization for large row counts to avoid loading all items</h3><p>SwiftData <code>Query</code> doesn&#8217;t currently support partial fetching. When rendering a large SwiftData dataset, consider using <code>FetchDescriptor</code> with appropriate predicates to paginate the data so you only load the data of the current page.</p><div><hr></div><h3>Can the sorting option in Swift Data queries take a user sort preference from <code>@AppStorage</code>?</h3><p>Yes, <code>SortDescriptor</code> is codable, and so you can definitely persist a sort descriptor to AppStorage, or other kind of storage, and then retrieve it from the storage when needed, and use it with a SwiftData query.</p><div><hr></div><h3>What if I want to store the sort property per trip, so each different trip&#8217;s sort setting is preserved</h3><p>In that case, If your trips grow unbounded you might want to consider persisting the sort options in your data model using SwiftData.</p><div><hr></div><h3>SwiftData and SwiftUI....I&#8217;ve found using <code>@Model</code> and <code>@Query</code> a reliable and easy to use approach for simple CRUD operations for a view. Is this the first go-to recommendation for implementing SwiftData in SwiftUI apps, when basics only are needed?</h3><p>That&#8217;s correct. You can start with <code>@Model</code> and <code>@Query</code>. If you have questions when going deeper, consider asking in the Developer Forums.</p><div><hr></div><h2>&#129695; SwiftUI Views &amp; Layout</h2><h3>Is List backed by UICollectionView? What is the most &#8220;CollectionView&#8221;- like <code>View</code> for SwiftUI?</h3><p><code>List</code> is the most &#8220;CollectionView&#8221;-like View for SwiftUI.</p><div><hr></div><h3>Given a list of cards that are mixed media (list of text rows, images, charts). Should you use Collection, List, ScrollView, or something else?</h3><p>It&#8217;s possible you would need all of them! for direct answers that are relevant to your project, I would make a post on the developer forums and link your code.</p><div><hr></div><h3>What happens with a <code>ViewThatFits</code> when no option actually fits? Does it render neither?</h3><p>Are you talking about <a href="https://developer.apple.com/documentation/swiftui/viewthatfits">https://developer.apple.com/documentation/swiftui/viewthatfits</a> ?</p><p>If a <code>ViewThatFits</code> option in SwiftUI does not find any view that fits within the specified size constraints, it typically renders nothing. This behavior is designed to conserve space by not displaying any content if no suitable view can be accommodated. The <code>ViewThatFits</code> view automatically adjusts its layout based on the available space, but when no view fits, it simply does not render. Check the link I sent you as the documentation is pretty good at how ViewThatFits works.</p><p>And check the sample code in the doc <a href="https://developer.apple.com/documentation/swiftui/viewthatfits">https://developer.apple.com/documentation/swiftui/viewthatfits</a></p><div><hr></div><h3>What might cause Text to sometimes be truncated? Using <code>.fixedSize(horizontal: false, vertical: true)</code> seems to always fix this issue. Is it due to ambiguous layout?</h3><p>It might not have a frame large enough to contain the content until you give it one. You can share the code and project on the forums to get input from engineers around the world. See here <a href="https://developer.apple.com/forums/">https://developer.apple.com/forums/</a></p><div><hr></div><h3>Silly question, for adaptive views why cannot I use <code>ZStack</code> over <code>.overlay()</code> or <code>.background()</code>?</h3><p>I don&#8217;t know what you mean, can you provide an example? Oh sorry use the forums to provide the example as will be a better answer if we can see what you are trying to accomplish as using <code>ZStack</code> over <code>.overlay()</code> or <code>.background()</code> for adaptive views isn&#8217;t necessary because both <code>ZStack</code> and these modifiers are part of SwiftUI&#8217;s declarative layout system and can work seamlessly together to manage stacking and overlaying of views.</p><p>Please provide your sample at the apple forums <a href="https://developer.apple.com/forums/">https://developer.apple.com/forums/</a></p><div><hr></div><h3>What is the best way to make a view inside a <code>ScrollView</code> at least the content size of the <code>ScrollView</code> itself and then it&#8217;s scrollable if larger than that. (Ex. I want to centre some empty/error state text).</h3><p>To achieve this effect, you&#8217;ll want to ensure that the view inside the has a minimum height equal to the height of the itself and grows if the content exceeds this size. With <code>GeometryReader</code> once outside the to capture the device&#8217;s screen height available for scrolling, and once inside to measure the content height. This setup will ensure the text is centered and the view is scrollable if the content exceeds the default screen/scrollable area height.</p><p>I can&#8217;t write code in chat very well, but something like that?</p><pre><code>GeometryReader { geometry in
    ScrollView {
        VStack {
            Spacer() Text("test test.")
                .multilineTextAlignment(.center).padding()
            Spacer() }
    }
}</code></pre><h2>&#128311; Observable &amp; State Management</h2><blockquote><p>Question about project from webinar</p></blockquote><h3>Why is <code>Trip</code> and <code>Activity</code> a class? I would have made them structs and only decorate DataSource with <code>@Observable</code>.</h3><p>You can only place <code>@Observable</code> with a class (not struct). This is because the <code>@Observable</code> macro is designed specifically for reference types (class) to enable observation of property changes. For value types like struct, you should use <code>@State</code> in your view to manage data changes, as structs are designed to be copied rather than shared.</p><div><hr></div><h3>To rephrase my question from earlier. I understand, that only classes can be annotated with <code>@Observable</code>. My question was more pointing towards why you chose class also for <code>Trip</code> and <code>Activity</code> in the sense of that DataSource is already observable, so all its properties are. Or am I getting it wrong?</h3><p>For the sample code, this allowed to pass the objects to views as references instead of copies, so that they could be mutated in place (when editing a <code>Trip</code>, adding activities to a trip, or marking an activity as completed). This made it so we wouldn&#8217;t need to mutate the whole model when a change was made.</p><div><hr></div><h3>Will <code>@StateObject</code> be updated to work with <code>@Observable</code> class without needing to conform the class to Combine&#8217;s <code>ObservableObject</code> protocol? We can&#8217;t use <code>@State</code> because it leaks heap memory, i.e. it inits a new initial object on every View init. And using optional State that&#8217;s init .<code>onAppear</code> is a painful</h3><p>If the object is created in View init and set to a <code>@State</code> property via <code>State(initialValue:)</code>, the new object instance is expected to be immediately freed if the view&#8217;s identity didn&#8217;t change, so leaking heap memory is not expected, and the leak might have been caused by other reasons like retain cycles. If there are concerns to create the <code>@Observable</code> object many times, an alternative would be to create the object higher up and pass it down to this view via a parameter in the init or via the environment.</p><div><hr></div><h3><code>@Environment</code> causes all views that contain it to update? So it is hard to make the views that use it stable? Any and every change causes all views to update ... Correct?</h3><p><code>@Environment</code> in SwiftUI causes views containing it to re-evaluate their state when the associated value changes.</p><p>If the environment value changes frequently or across the app, consider using or directly within the relevant view hierarchy to encapsulate logic and data handling. Use conditional rendering to avoid unnecessary re-renders.</p><p>Yes they cause to update but while provides a powerful mechanism for data sharing, manage its usage carefully to maintain UI stability and responsiveness. By leveraging SwiftUI&#8217;s declarative nature and state management capabilities, minimize unnecessary re-renders and ensure a smooth user experience.</p><p>Main benefit it to be used to share state objects accessed by multiple views.</p><div><hr></div><h3>Can you subclass an <code>@Observable</code> object so that parent and child would have the macro? Example: </h3><pre><code>@Observable
class Trip {}

@Observable class ExtendedTrip: Trip {}</code></pre><p>Are you talking about the sample or in general on SwiftUI? You can subclass an in Swift to create a custom observable object that can be used across the view hierarchy. This allows you to encapsulate your data and logic in a way that can be observed by any views that depend on it.</p><p>This pattern allows you to encapsulate your data and logic in a reusable observable object that can be shared across different views in your SwiftUI app. Like this? </p><pre><code>@ObservedObject var observableObject: CustomObservableObject</code></pre><div><hr></div><h3>In SwiftUI lists, should item model be value types (struct) and rely on parent ViewModel updates, or reference types (<code>Observable/ObservableObject</code>) so each row can update independently? What&#8217;s the recommended trade-off for performance and architecture?</h3><p>This question is being answered in the current session. Please tune into the live Data Flow session to learn more.</p><p>The answer is&#8230; it depends! Use structs when your data is of a value type, and use class when your data contains reference types. When using a class, you typically make it Observable, so a change on the data can update related SwiftUI views.</p><div><hr></div><h3>I want to ask a quite basic question. If we are creating a model we should always be using class instead of struct? And there is no such thing as &#8220;View Model&#8221; then?</h3><p>You normally use struct when your data is of a value type, and use class when your data contains reference types. When using a class, you typically make it <code>Observable</code> so a change on the data can trigger relevant SwiftUI updates.</p><div><hr></div><h3>I have an <code>ObservedObject</code> with Published Properties associated with a View. This is extended into 3 <code>ObservedObjects</code> conforming to a common Protocol. But the properties of the protocol would actually be Published Properties, which is not possible? Can we define a Protocol with such properties?</h3><p>Swift doesn&#8217;t allow adding a stored property to a protocol extension, so if you tend to add published properties to a protocol extension so the types conforming the protocol automatically gain the properties, you can&#8217;t do so. You might consider using an Observable class instead. For more information, see mentioned in the following doc.</p><div><hr></div><h3>Could you explain the difference and why using <code>let</code> and <code>var</code> inside Views. It looks like with <code>let</code> the whole View is redrawn when the value change in the caller View.</h3><p>When developing SwiftUI views, understanding the differences between and is crucial for efficient view updates and correct behavior. <code>Let</code>: Creates a constant value that cannot be modified after initialization. <code>Var</code>: Allows properties to change over time, triggering view updates.</p><p>Use let for stable values that do not change during view updates, ensuring predictable view behavior. Dynamic data in SwiftUI can be managed using <code>var</code> with <code>@State</code> for interactive or changing data, enabling reactive UI updates.</p><p>Mastering <code>let</code> and <code>var</code> in SwiftUI views is crucial for efficient, responsive user interfaces. Use <code>let</code> for immutable data and <code>var</code> with <code>@State</code> for mutable state</p><p>I would recommend to post this question into the forums as the basics of let and var in the views is probably not enough for you.</p><p>Visit Apple Developer Forums here: <a href="https://developer.apple.com/forums/">https://developer.apple.com/forums/</a></p><div><hr></div><h2>&#128655; Navigation</h2><h3>How did you migrate from UIKit to SwiftUI specifically in navigation? Did you have a coordinator patter?</h3><p>It depends on what exactly you&#8217;re trying to achieve here. The coordinator patter works as one way to handle navigation but it&#8217;s not required. The best thing to note, is that if you&#8217;re in UIKit <code>UINavigationController</code> you will want to use the standard push/present calls and present a <code>UIHostingController</code>. And inside a SwiftUI context you will want to use SwiftUI presentation. Mixing the navigation stacks can have unintended consequences. If a coordinator patter helps manage that, that works perfectly but you can also just call the views directly depending on the complexity of the UI and what fits best.</p><div><hr></div><h3>Migrating from UIKit to SwiftUI has two major difficulties, and they are not on View, it&#8217;s navigation and database. Do you have a presentation or a study case for this? How to move from segue navigation to SwiftUI? How to move from Core Data context to Swift Data?</h3><p>A great video for that and resource I like is: <a href="https://developer.apple.com/videos/play/wwdc2023/10189/">https://developer.apple.com/videos/play/wwdc2023/10189/</a></p><p>Migrating from UIKit&#8217;s segue-based navigation to SwiftUI&#8217;s navigation system involves understanding the differences between the two approaches and adapting your navigation logic accordingly.</p><p>To migrate to SwiftUI, you&#8217;ll need to create a SwiftUI view that represents the same flow. Instead of using a navigation controller, you&#8217;ll use SwiftUI&#8217;s navigation views.</p><p>Also this is my go to <a href="https://developer.apple.com/documentation/swiftui/building-a-document-based-app-using-swiftdata">https://developer.apple.com/documentation/swiftui/building-a-document-based-app-using-swiftdata</a></p><p>Bring core data to swiftUI <a href="https://developer.apple.com/videos/play/wwdc2021/10017/">https://developer.apple.com/videos/play/wwdc2021/10017/</a></p><p>And remember to model your data <a href="https://developer.apple.com/videos/play/wwdc2023/10195/">https://developer.apple.com/videos/play/wwdc2023/10195/</a></p><p>I am not an expert in SwiftData so I would recommend to ask that question and be specific of what you want migrate in another question for our expert to pick up.</p><div><hr></div><h3>How can we best provide async navigation links? This seems to be a fundamental requirement. I built a custom built <code>AsyncNavigationLink</code> that overlays a button on a <code>NavigationLink</code>, triggers an async call, overlays a <code>ProgressView()</code>. The async call triggers the <code>NavigationLink</code>. Is this the best way?</h3><p><code>NavigationLink</code>s themselves can&#8217;t be responsible for asynchronous handling. The destination view can have a <code>ProgressView</code> and react to asynchronous changes to your model. But, since <code>NavigationLink</code> is a view itself, it needs to be processed on the main actor. If you want to present the <code>ProgressView</code> <em>before</em> moving to the new view, you can make a <code>Button</code> that triggers the asynchronous callback, and then update your navigation path (that you can set with <code>NavigationStack(path: $path)</code>) when it is finished. This will use the <code>.navigationDestination(for:)</code> modifiers to decide the views to move to. Not sure if this entirely answers your question (</p><div><hr></div><h3>Navigation in Swift is one of the topics I&#8217;ve struggled with for a while. Whether using <code>NavigationDestination</code> or building your own Router to navigate. What is your recommendation for doing such, especially when you have multiple views that you want to push? Any suggestions?</h3><p>The developer website has tons of documentation on recommended navigation styles: See <a href="https://developer.apple.com/documentation/swiftui/bringing-robust-navigation-structure-to-your-swiftui-app">https://developer.apple.com/documentation/swiftui/bringing-robust-navigation-structure-to-your-swiftui-app</a> and <a href="https://developer.apple.com/documentation/swiftui/navigation">https://developer.apple.com/documentation/swiftui/navigation</a></p><div><hr></div><h2>&#128736;&#65039; UIKit &amp; SwiftUI Integration</h2><h3><code>HostingConfiguration</code> or <code>HostingController</code> ? On <code>TableView</code> and <code>CollectionView</code> , what&#8217;s best way to wrap SwiftUI code in cells? Is there difference between hosting config and hosting controller?</h3><p>For wrapping a SwiftUI hierarchy in <code>UICollectionViewCell</code> or <code>UITableViewCell</code> objects, <code>UIHostingConfiguration</code> is the intended class. Please see the documentation for details: <a href="https://developer.apple.com/documentation/swiftui/uihostingconfiguration/">https://developer.apple.com/documentation/swiftui/uihostingconfiguration/</a> UIHostingController is intended to be used more broadly for SwiftUI views in general.</p><div><hr></div><h3>What is the best way to integrate SwiftUI into an existing UIKit App, keeping the performance of SwiftUI Previews high? I can use Swift Package Manager to integrate packages that contain SwiftUI components into the project. Is this the best way or are there simpler paths to go for best speed?</h3><p>To use SwiftUI views in your UIKit app. See UIKit integration <a href="https://developer.apple.com/documentation/swiftui/uikit-integration">https://developer.apple.com/documentation/swiftui/uikit-integration</a></p><div><hr></div><h3>I&#8217;d like to re-ask the question by Joshua Arnold about resizing with <code>UIViewRepresentable</code>, as the link provided does not give an answer as to how to animate the size change</h3><p>The reference is at the bottom of the linked page, see Adding UIKit to SwiftUI View Hierarchies <a href="https://developer.apple.com/documentation/swiftui/uiviewrepresentable#Adding-UIKit-views-to-SwiftUI-view-hierarchies">https://developer.apple.com/documentation/swiftui/uiviewrepresentable#Adding-UIKit-views-to-SwiftUI-view-hierarchies</a></p><p>You can also post your code, images and more on the forums to receive direct help <a href="https://developer.apple.com/forums/">https://developer.apple.com/forums/</a></p><div><hr></div><h3>When using <code>UIViewRepresentable</code>, is it possible for the <code>UIView</code> to internally resize itself, and have SwiftUI animate the change in size? If I call <code>invalidateIntrinsicContentSize()</code> on the view, SwiftUI resizes the view immediately without an animation.</h3><p>You must communicate size changes back to SwiftUI state for animations to work. For info how see Adding UIKit views to SwiftUI view hierarchies <a href="https://developer.apple.com/documentation/swiftui/uiviewrepresentable">https://developer.apple.com/documentation/swiftui/uiviewrepresentable</a></p><div><hr></div><h3>What is Apple&#8217;s recommended approach for detecting when a <code>UIHostingConfiguration</code> / <code>ContentView</code> changes size internally? If I update the a <code>UIHostingConfiguration</code> with a larger view, the view will just truncate / clip instead of bubbling up to UIKit to resize itself.</h3><p>When working with a hybrid UIKit and SwiftUI environment, SwiftUI views may change size internally without automatically updating their containing UIKit views. This is because SwiftUI views are declarative and may not notify UIKit of size changes beyond their initial setup. To address this, you can use SwiftUI view modifiers and UIKit mechanisms to propagate size changes.</p><p><code>GeometryReader</code> to read the current size of a SwiftUI view and communicate changes back to UIKit.</p><p>I would personally wrap a SwiftUI view in a <code>UIViewRepresentable</code> and use a coordinator to listen for size preference changes and update the UIKit view size accordingly.</p><p>in a UIKit view controller or view to adjust the layout so observe notifications if possible</p><p>Visit Apple Developer Forums here: <a href="https://developer.apple.com/forums/">https://developer.apple.com/forums/</a></p><div><hr></div><h2>&#128311; ScrollView &amp; Visibility</h2><h3>Is there any further documentation or reliable ways to know if something is actually visible on screen? Based our experiences, onAppear in <code>VStack</code> in ScrollView, List, and LazyVStack all have different onAppear behaviors for their elements.</h3><p>Please check out the <code>.onScrollVisibilityChange</code> <a href="https://developer.apple.com/documentation/swiftui/view/onscrollvisibilitychange(threshold:_:)">modifier</a>.</p><div><hr></div><h3>Is there a suggested way to determine the difference between <code>.onAppear</code> being called in a &#8216;forward&#8217; transition vs a &#8216;return&#8217; transition? i.e. list displayed vs returning to list from details view.</h3><p>Yes, it is possible to differentiate between when <code>.onAppear</code> is called during a forward transition versus a return transition in SwiftUI, though it requires some manual state management since SwiftUI itself does not provide built-in distinction for these cases directly within <code>.onAppear.</code> I got some sample code so you can provide a more detailed information about your question.:</p><pre><code>.onAppear { 
    if appearedOnce {
            print(&#8221;Main View reappeared (back navigation)&#8221;) 
    } else { 
        print(&#8221;Main View appeared (forward navigation)&#8221;)
        appearedOnce = true 
    } 
}</code></pre><div><hr></div><h3>If I have a database with 100k+ rows, how do I manage paging/infinite scroll in SwiftUIs &#8220;List&#8221;, to manage memory appropriately. Similar to <code>UITableViews</code> prefetch handler.</h3><p>Similar the the <code>VStack</code> and <code>HStack</code> shown on stage, there is a <code>LazyVStack</code> and <code>LazyHStack</code> which automatically allocate and deallocates views as they are needed on screen. For more info see &#8220;Creating performant scrollable stacks&#8220;: <a href="https://developer.apple.com/documentation/swiftui/creating-performant-scrollable-stacks">https://developer.apple.com/documentation/swiftui/creating-performant-scrollable-stacks</a></p><div><hr></div><h2>&#129419; Animation &amp; Effects</h2><h3>For the <code>symbolEffect(_:options:value:)</code> method for SF symbols, the animation only runs when the value changes. Is there a way to implement this behavior (only run something once for each unique value) idiomatically in SwiftUI?</h3><p>Depending on the use case, this might not require explicit treatment to run an animation (e.g. the <code>animation(value:)</code> modifier might be all you need). But if you do need to perform side effects based on value changes, <code>onChange(of:...)</code> might be right to reach for.</p><div><hr></div><h3>I&#8217;ve added an animation to a Button but when it&#8217;s placed inside a <code>NavigationStack&#8217;s</code> <code>ToolbarItem</code>, the animation doesn&#8217;t run. When the <code>Button</code> is used elsewhere, the animation works as expected. Can you share how to use this animating <code>Button</code> inside a <code>.toolbar</code>?</h3><p>This is a good question for the forums. Given a <code>Button</code> inside a <code>.toolbar { }</code> modifier, we&#8217;d need to see a fuller code sample to know things like where the animation is declared.</p><div><hr></div><h3>How can I start an animation after another animation? (without using hard coded durations)</h3><p>A <code>KeyframeAnimator</code> or <code>PhaseAnimator</code> can be used for more complex animations! In <code>KeyframeAnimator</code>, for example, you can have separate steps in a <code>KeyframeTrack</code>, and separate tracks in a <code>KeyframeTimeline</code>. Helpful docs: <a href="https://developer.apple.com/documentation/swiftui/controlling-the-timing-and-movements-of-your-animations">https://developer.apple.com/documentation/swiftui/controlling-the-timing-and-movements-of-your-animations</a> <a href="https://developer.apple.com/documentation/swiftui/keyframetimeline">https://developer.apple.com/documentation/swiftui/keyframetimeline</a></p><div><hr></div><h2>&#9935;&#65039; Modifiers &amp; Customization</h2><h3>Is it possible to write modifier functions that can configure a custom view property even if it is contained in other container views? This would be similar to how <code>.font</code> works, in that it can be applied to a view and any contained <code>Text</code> will be reconfigured?</h3><p>One possible way to achieve this is to use a type such as <code>Group</code> that can create a collection of views, such that its modifiers apply to all of its member views: <a href="https://developer.apple.com/documentation/swiftui/group">https://developer.apple.com/documentation/swiftui/group</a></p><div><hr></div><h3>UIKit is inheritance-driven, so it is possible to subclass an <code>UIButton</code>, for example. The same is not possible with SwiftUI. What is the recommended alternative for a similar approach (mainly to customize appearance)? Style, viewModifiers, custom view, something else?</h3><p>Custom view modifiers are one way in which you can reuse multiple view modifiers across different views. You can even extend the View protocol with a function that applies your custom modifiers. Check out &#8220;Reducing view modifier maintenance&#8221; for how to do that. <br><a href="https://developer.apple.com/documentation/swiftui/reducing-view-modifier-maintenance">https://developer.apple.com/documentation/swiftui/reducing-view-modifier-maintenance</a></p><div><hr></div><h3>Is it possible to add a view modifier for the text width and set it to expanded as well as making it italic? The following code does not work, and italic is only applied if the width is .<code>standard:</code></h3><pre><code>.font(.caption2.width(.expanded).italic())
   .fontWeight(.bold)
   .frame(maxWidth: width, alignment: .leading)
   .italic(italic)</code></pre><p>Something like that too: <code>.font(.caption2)</code></p><p>it is possible to combine text width and italic styling in SwiftUI using a custom view modifier. The issue with your current code is that the modifier in SwiftUI does not support applying both and modifiers directly in a combined way like you&#8217;re attempting. Instead, you can create a custom view modifier to achieve this behavior</p><p>If I put it together should be something like:</p><pre><code>content .font(.caption2) .fontWeight(.bold).frame(maxWidth: width, alignment: .leading) .italic(italic)</code></pre><p>Disclamer I didn&#8217;t test it, but I think is the idea.</p><p>Allows you to flexibly adjust the text&#8217;s width and apply italic styling independently within a single view modifier.</p><div><hr></div><h3>What&#8217;s the best approach to building modifiers in SwiftUI? I am specifically interested in how to handle the inert state. Is it better to apply the modifier conditionally depending whether it&#8217;s inert state or not. Or is it better to deal with the sate inside the modifier implementation.</h3><p>Generally, try to avoid conditionally applying modifiers (specifically when the condition can change at runtime). It is better to handle an &#8220;inert&#8221; state from within the modifier. If you have a specific question about building a modifier that handles some state, feel free to send a question for that as well!</p><div><hr></div><h2>&#128270; Performance &amp; Debugging</h2><h3>I&#8217;m trying to debug a log about &#8220;Attribute Graph Cycle&#8221;. I&#8217;ve tried instruments, but it&#8217;s hard to identify the source. In my case, I think it has to do with frames/geometry negotiating sizes. I don&#8217;t notice a major slowdown - Is this even a problem?</h3><p>Check if you have a custom layout or <code>onGeometryChange</code> that could be affecting your View&#8217;s sizing. Instruments can help show the catalyst of an update (through the Cause-and-Effect graph). Hitting a cycle may lead to unexpected behavior (aside from just performance issues/hangs), so it is still important to identify if you can. If you don&#8217;t see anything in your program that could be causing a loop, consider filing a bug report on Feedback Assistant!</p><div><hr></div><h3>Is there a way to see SwiftUI&#8217;s dependency graph for a view, so we can figure out whats causing excessive updates etc? Is instruments the best way to do this?</h3><p>Instruments is the best way to figure out what is causing excessive updates and other issues with performance. Check out &#8220;<a href="https://developer.apple.com/videos/play/wwdc2025/306/">Optimize SwiftUI performance with Instruments</a>&#8221; for a lot of great information about SwiftUI and how to use Instruments to profile and optimize your app.</p><div><hr></div><h3>Can we see the view&#8217;s dependency graph in Xcode? Via debugging?</h3><p>If you profile using SwiftUI template in Instruments, it shows you information such as long view body updates, cause-and-effect graph, update groups etc. please review the <a href="https://developer.apple.com/documentation/Xcode/understanding-and-improving-swiftui-performance">Understanding and improving SwiftUI performance</a> article to learn how to use instruments to understand and debug your SwiftUI Views.</p><div><hr></div><h3>When is it better to extract a view into a separate <code>ViewBuilder</code> function/computed property versus creating a separate struct View? What are the performance implications of each approach?</h3><p>Extracting your views into a separate view structs is a good way to, reduce dependencies to only those that matter to the view. This makes your code easier to read, and your dependencies become apparent at its use site. Ultimately you want to use your best judgement here because not every dependency deserves to be scoped to a separate view struct.</p><p>When views are scoped to separate view struct, try reducing view values to only the data they actually depend on. This could mean fewer updates and that implies better performance when data changes in your app.</p><div><hr></div><h3>During SwiftUI view reordering or layout updates, what typically causes temporary disappearance of views, and how can state consistency be preserved during rapid UI changes?</h3><p>Having views temporarily disappear isn&#8217;t typical. Check out the tips in &#8220;<a href="https://developer.apple.com/videos/play/wwdc2025/306/">Optimize SwiftUI performance with Instruments</a>&#8221; to determine if there are some performance issues or tuning that you can do to improve the view. If that doesn&#8217;t help, please post your specific issue on the Developer Forums for more assistance.</p><div><hr></div><h3>One of the great things I find in working with SwiftUI is that you can build most of your app staying in Xcode with Preview. I loved the last section about using <code>print</code> in view, but it is only at runtime. Is there a library that let you use a debug overlay modifier to display debug data in a view?</h3><p>You can output into a Label? And there are many views where output is allowed. But the NSLOG is a way to go to the console. You can add a GeometryReader within a view&#8217;s <code>.overlay()</code></p><p>SwiftUI provides a convenient way to display debug data</p><p>There is a video that may help you: <a href="https://developer.apple.com/videos/play/wwdc2023/10226">WWDC23: Debug with structured logging </a></p><p>I would recommend this doc as well <a href="https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/DebuggingTricksandTips.html">https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/DebuggingTricksandTips.html</a></p><div><hr></div><h3>Are there tools for diagnosing layout issues? On macOS, my app behaves very strangely, growing the window arbitrarily even though subviews&#8217; sizes aren&#8217;t changing.</h3><p>Hi, you might find the following guide helpful: &#8220;<a href="https://developer.apple.com/documentation/xcode/diagnosing-issues-in-the-appearance-of-your-running-app">Diagnosing issues in the appearance of a running app</a>&#8221;.</p><div><hr></div><h2>&#127969; Dismissal &amp; Environment</h2><h3>What would be the difference in using the <code>@Enviroment(.dismiss)</code> or making it a binding when dismissing a sheet? Is there performance benefit?</h3><p>Both and make sheet dismissal in SwiftUI, but they differ in functionality and performance. It is a property wrapper that exposes the modifier applied to a view. It&#8217;s often used to dismiss views or sheets from outside their hierarchy, like from a parent view or navigation stack. It is efficient as it doesn&#8217;t create additional state or UI components. It simply exposes an existing closure in the view hierarchy, involves defining a closure for dismissal and wrapping it in a . This is useful for more control or integrating with custom views that require explicit dismissal but creating a binding has some overhead, but it&#8217;s usually negligible. The impact is minimal compared to other UI operations.</p><p>In summary, and are efficient is straightforward for dismissing views from outside their hierarchy, while binding provides more control and flexibility for custom dismissal logic. Performance differences are minimal I think, and the choice depends on your application&#8217;s requirements.</p><div><hr></div><h2><strong>&#127963;&#65039;</strong> Architecture &amp; Patterns</h2><h3>Is MVVM organization recommended for a SwiftUI project?</h3><p>Different apps need different architectures. An app with heavily server-driven content should make architectural decisions specific to that design. An app focused more on displaying data locally (and might not be updating its content as frequently) should use an architecture that makes that use case easier to build for. SwiftUI is architecture agnostic to allow deliberate architectures that work for your app.</p><h3>When passing in an action into a view is it better to use closure or function/method reference? Example:  </h3><pre><code>SomeView(saveAction: { viewModel.performSave() }) 
vs
SomeView(saveAction: viewModel.performSave)</code></pre><p>Great question and thanks for providing the code. What I think is whether to use a closure or a function/method reference when passing an action into a view depends on your specific use case and personal preference. Both approaches have their advantages, and the choice might be influenced by factors like readability, performance, and Swift features. You can capture specific variables from the surrounding context using a capture list if needed, providing more flexibility. It allows for quick inline modifications or additional logic without needing to create a new method elsewhere. Cleaner syntax, especially for simple method calls, improving readability. Potentially more performant since it directly references the function without additional closure overhead?</p><div><hr></div><h2>&#128030; iOS Issues &amp; Bug Reporting</h2><h3>Since iOS26, dismissing the keyboard stopped working as expected. If I have a <code>TextField</code> focused in a <code>navigationDestination</code> view or in the content view of a <code>fullScreenCover</code>, and then dismiss the view, the keyboard would sometimes leave a white bar at the bottom in the safeArea. Is this a known issue?</h3><p>If you come across any visual issues while using SwiftUI, please feel free to file a bug report via Feedback Assistant. Thanks for letting us know about this! <a href="https://developer.apple.com/bug-reporting/">https://developer.apple.com/bug-reporting/</a></p><div><hr></div><h2>&#127891; Learning Resources &amp; Miscellaneous</h2><h3>What&#8217;s the best way to learn how to use Metal, particularly for visual effects rather than graphics?</h3><p>This is just my approach, I like to take apart and study sample code. For Metal, there is an entire database of sample code here <a href="https://developer.apple.com/metal/sample-code/">https://developer.apple.com/metal/sample-code/</a></p><p>Also, please check out the segment on Metal in this talk from WWDC24, &#8220;<a href="https://developer.apple.com/videos/play/wwdc2024/10151/?time=1180">Create custom visual effects with SwiftUI</a>&#8221;.</p><div><hr></div><h3>Love to learn more about SwiftUI and Metal - for video rendering would the performance be faster using a custom shader with [[stitchable]] and SwiftUI vs NSView or MkitView?</h3><p>It depends on the purpose! <code>[[stitchable]]</code> Metal shaders can be used to apply an effect to an existing SwiftUI view via the <code>colorEffect(_:isEnabled:)</code>, <code>distortionEffect(_:,maxSampleOffset:isEnabled)</code>, and <code>layerEffect(_:maxSampleOffset:isEnabled:)</code> modifiers. If you&#8217;re looking for a more flexible view that displays Metal objects (i.e. drawing your own shapes with <code>vertex</code> and <code>fragment</code> shaders), you should probably lean on <code>MTKView</code>. <code>MTKView</code> is provided by MetalKit and can be placed in an <code>NSViewRepresentable</code> or <code>UIViewRepresentable</code>. One is not strictly faster than the other, and you should choose whichever method matches your vision! MTKView: <a href="https://developer.apple.com/documentation/metalkit/mtkview">https://developer.apple.com/documentation/metalkit/mtkview</a> SwiftUI Shader: <a href="https://developer.apple.com/documentation/swiftui/shader">https://developer.apple.com/documentation/swiftui/shader</a></p><div><hr></div><h3>What do your designers (in Apple) use to design for passing on to developers? Whenever I&#8217;ve had designs from Figma, Sketch, Azure, Miro, whatever, they aren&#8217;t true to platform and don&#8217;t correctly confine the designer to use native components.</h3><p>This is a good question! It can be hard for design tools to match the fidelity of the look and feel of Apple platforms. Apple provides design resources for Sketch and Figma at <a href="https://developer.apple.com/design/resources/">https://developer.apple.com/design/resources/</a> and the materials are adjusted to resemble iOS as closely as possible.</p><p>In your partnership with developers, check in with the engineers and ensure that the designs are well understood before they are implemented. It also helps when everyone communicates using the same language, so we recommend that both developers and designers (and even other members of the team) familiarize themselves with the Human Interface Guidelines for the platform your app supports. The Human Interface Guidelines are available at <a href="https://developer.apple.com/design/human-interface-guidelines/">https://developer.apple.com/design/human-interface-guidelines/</a></p><div><hr></div><h3>I had some trouble finding the entry point of sessions like &#8220;Code-along: Start building with Swift and SwiftUI&#8221; on the developer website. Is there an entry point where I can navigate thru all the sessions like &#8220;code along&#8221; and today&#8217;s or previous sessions?</h3><p>Thanks for the question. I recommend to start with this video: <a href="https://developer.apple.com/videos/play/wwdc2021/10062/">https://developer.apple.com/videos/play/wwdc2021/10062</a>, then a code along is always good like: <a href="https://developer.apple.com/videos/play/meet-with-apple/237/">https://developer.apple.com/videos/play/meet-with-apple/237/</a> and do not forget the what&#8217;s new to avoid using the older API <a href="https://developer.apple.com/videos/play/wwdc2024/10136/">https://developer.apple.com/videos/play/wwdc2024/10136/</a></p><div><hr></div><h3>As an absolute beginner, what would be the first go-to step to go for training? or documentation? on Apple.</h3><p>A great place to learn how to develop for Apple is through Pathways (<a href="https://developer.apple.com/pathways/?cid=ht-pathways">https://developer.apple.com/pathways/?cid=ht-pathways</a>), your first step to creating for Apple platforms! For documentation, here is the top-level page: <a href="https://developer.apple.com/documentation/">https://developer.apple.com/documentation/</a></p><div><hr></div><h3>Do you have any advice for someone with Flutter/Dart experience starting SwiftUI?</h3><p>Thanks for the question. I think we got something similar from someone else, but welcome to Swift coming from Flutter! To start I would recommend to start with this video: <a href="https://developer.apple.com/videos/play/wwdc2021/10062/">https://developer.apple.com/videos/play/wwdc2021/10062/</a> then a code along is always good like: <a href="https://developer.apple.com/videos/play/meet-with-apple/237/">https://developer.apple.com/videos/play/meet-with-apple/237/</a> and do not forget the what&#8217;s new to avoid using the older API <a href="https://developer.apple.com/videos/play/wwdc2024/10136/">https://developer.apple.com/videos/play/wwdc2024/10136/</a> But welcome to Xcode! You are going to love it.</p><div><hr></div><h3>Any recommendations for resources (books, WWDC sessions, sample repos, or articles) on doing test-driven development with Swift/SwiftUI&#8212;especially patterns for testing view models/presenters and keeping UI code thin?</h3><p>There&#8217;s several WWDC sessions that may be beneficial to you, <a href="https://developer.apple.com/videos/play/wwdc2024/10195">https://developer.apple.com/videos/play/wwdc2024/10195</a> and <a href="https://developer.apple.com/videos/play/wwdc2019/413">https://developer.apple.com/videos/play/wwdc2019/413</a></p><div><hr></div><h3>There are tons of modifiers, how we suppose to memorize all of them or even know that they are exists? &#128522;</h3><p>The Developer Documentation is always a great place to start! <a href="https://developer.apple.com/documentation/swiftui">https://developer.apple.com/documentation/swiftui</a> You can also look into bundling your commonly-used modifiers into custom view modifiers- check out &#8220;<a href="https://developer.apple.com/documentation/swiftui/reducing-view-modifier-maintenance">Reducing view modifier maintenance</a>&#8221;.</p><div><hr></div><h2>&#128311; Wishlist App Specific (App from the webinar)</h2><h3>Where can I download the wishlist app xcode project?</h3><p>Thank you for your question and attending the event. The sample code for &#8220;SwiftUI foundations: Build great apps with SwiftUI&#8221; will be made available on the Apple developer website sometime after the event concludes. Stay tuned!</p><div><hr></div><h3>Will the code for Wishlist app be made available?</h3><p>Thank you for your question and attending the event. The sample code for &#8220;SwiftUI foundations: Build great apps with SwiftUI&#8221; will be made available on the Apple developer website sometime after the event concludes. Stay tuned!</p><div><hr></div><h3>Why cardsize is provided for <code>TripCard</code> in example of Wishlist struct presented in motion presentation ?</h3><p><code>TripCard</code> could either be represented in different sizes, either as a compact or expanded card with a larger width.</p><div><hr></div><h3>I noticed the Wishlist sample app has a customized font in the <code>NavigationBar&#8217;s</code> title. Curious how this was accomplished?</h3><p>For a navigation bar title with styling, you can create a custom view containing a Text with styling modifiers, then wrap it in <code>ToolbarItem(placement: .largeTitle)</code> or <code>ToolbarItem(placement: .title)</code> on the <code>.toolbar</code>.</p><div><hr></div><h3>Is the Wishlist app MultiPlatform for iPhone, Watch, TV, Mac, etc?</h3><p>Thanks for the question. The app is currently an iOS / iPadOS app. You can run it on macOS and visionOS in Designed for iPad mode.</p><div><hr></div><h2>&#128172; Text &amp; Fonts</h2><h3>Any recommendations to have a more readable or idiomatic alternative to <code>.frame(maxWidth: .infinity, alignment: .leading)</code>?</h3><p>Certainly! If you&#8217;re looking for a more readable or idiomatic alternative to , it depends on the context and the layout you&#8217;re working with. If you&#8217;re using Auto Layout, you can define constraints programmatically. This might be more readable if you&#8217;re comfortable with constraints, use that. If you have a container view that naturally supports full-width content, you might use a container view to manage the layout. If you need more control over the frame, you might define a custom frame Choose the method that best fits your design system and the constraints of your project. Auto Layout is often preferred for its flexibility and control, especially in UIKit. But I personally like adding a <code>Spacer()</code> into the view!.</p><div><hr></div><h3>I would like to follow up on the question on how to add a different font in the <code>navigationTitle</code>. While it is true, that you can add a styled text to the modifier. The result will will compile but not change its appearance and show an error: &#8220;Only unstyled text can be used with <code>navigationTitle(_:)</code>&#8221;.</h3><p>You can create a <code>ToolbarItem(placement: .title/.largeTitle)</code> that is a <code>Text</code> styled with <code>AttributedString</code>.</p><div><hr></div><h2>&#128208; Geometry &amp; Sizing</h2><h3>What if a subview prefers a bigger HxW than what is offered by the parent view? How are the conflicts handled?</h3><p>I&#8217;d recommend testing this out, then adding other modifiers to build intuition for SwiftUI&#8217;s layout system! </p><pre><code>struct BiggerChild: View { 
  var body: some View {
     VStack {
       Color.orange .frame(width: 200, height: 200) .opacity(0.3) 
      }
      .frame(width: 100, height: 100).background(.purple) 
    } 
}</code></pre><p>You&#8217;ll find that the parent won&#8217;t clip the child, and the child will extend beyond the parent. From there, check out the <code>clipShape()</code> modifier.</p><div><hr></div><h3>Is there a canonical way to set .frame sizes that are dynamic to device sizes? For example, <code>.frame(height: oneThirdOfDeviceHeight)</code>? <code>UIScreen</code>? Geometry reader? Is there a preferred approach?</h3><p>To set dynamic frame sizes in Swift for different devices, you can calculate dimensions based on screen width and a predefined ratio to maintain aspect ratios. The GeometryReader approach is modern and powerful, defining views in terms of available space. For complex layouts, Auto Layout provides flexible constraints that adapt to device sizes. For simplicity and direct access, using or similar methods works well for basic dynamic sizing. For more complex layouts and better responsiveness, Auto Layout is more powerful. Constants and computed properties help keep code organized and maintainable.</p><p>I think I understand your question with the frames utilization but please feel free to ask the question again with more details.</p><div><hr></div><h3>I have the following: </h3><pre><code>LazyVGrid { 
  ForEach { 
    VStack { 
      Image() 
      Text() 
    } 
   } 
}</code></pre><h3>Now, because the text of each view differs (sometimes it takes 2 lines, sometimes 1 line and sometimes 3 lines) the views are no longer aligned on a base-line. How can I get the views aligned again at a common base-line.</h3><p>It could be the way your <code>LazyVStack</code> is deallocating views. I think this would be a great question for the Developer Forums. I encourage you to post this question there so we can more easily share code and others with the same issue can find help for the same issue. Visit Apple Developer Forums here: <a href="https://developer.apple.com/forums/">https://developer.apple.com/forums/</a></p><div><hr></div><h3>If I have a view thats 50px x 50px, and I want to display many of them in a HStack, evenly spaced so they are edge to edge. Whats the most performant way to achieve this? I&#8217;ve been using a &#8220;<code>Spacer()</code>&#8221; inbetween each, but wondering if thats causing overhead</h3><p>Best way to see the best perform control is to debug and use instruments to check your UI as has SwiftUI instruments as well. Using between views in an is a common approach, but if you&#8217;re concerned about performance due to a large number of views, you might consider a more direct calculation to distribute space evenly. Lately I have been a huge fan of <code>GeometryReader</code> as It is used to get the available width of the parent view. Calculate the total width occupied by the views and subtract it from the available width to get the total spacing needed. Divide this by the number of gaps (which is one less than the number of views).</p><p>But Space is really good, I would really recommend you to make Instruments your best friend.</p><p>Visit Apple Developer Forums here: <a href="https://developer.apple.com/forums/">https://developer.apple.com/forums/</a></p><div><hr></div><h3>Any performant alternatives to geometry reader? I have to put a vertically centered textfield, with a helper text below it, but also add pull to refresh, so I have added a scrollview.refreshable, one alternative is to create a manual drag gesture, but can we achieve this using scrollview?</h3><p>This depends on what you&#8217;re trying to achieve. For example, if you&#8217;re trying to respond to scroll view changes you should checkout the <a href="https://developer.apple.com/documentation/swiftui/view/onscrollgeometrychange(for:of:action:)">onScrollGeometryChange</a>, <a href="https://developer.apple.com/documentation/swiftui/view/onscrolltargetvisibilitychange(idtype:threshold:_:)">onScrollTargetVisibilityChange</a>, <a href="https://developer.apple.com/documentation/swiftui/view/onscrollvisibilitychange(threshold:_:)">onScrollVisibilityChange</a><br> and <a href="https://developer.apple.com/documentation/swiftui/view/onscrollphasechange(_:)">onScrollPhaseChange API</a>.</p><div><hr></div><h2>&#128252; Images &amp; Media</h2><h3>I&#8217;d like to have a zoomable and draggable image within a scrollview, like you would see in the photos app. Can this be done with SwiftUI?</h3><p>We can provide information how Apple apps controls work, you can achieve a zoomable and draggable image within a in SwiftUI using the modifiers and the and views.</p><pre><code>.gesture( DragGesture() .onChanged { value in 
  print(&#8221;Dragging at: (value.location)&#8221;) 
}.onEnded { value in 
  print(&#8221;Drag ended at: (value.location)&#8221;)
})</code></pre><p>Other this are <code>ScrollView</code>, creates a horizontal <code>ScrollView</code> without showing the scroll indicator bar.</p><h3>If I present a series of Image views in a <code>LazyVStack</code> -- Image views that need to be resized. How can I make sure all images get resized to the same width/hight (a common denominator) and don&#8217;t end-up looking different?</h3><p>To ensure consistent sizing of views in a series, explicitly set modifiers on each view to define a specific width and height for all images. Decide if you want to maintain the aspect ratio by using modifiers to control how images fit within their frames. This makes images scalable, sets a fixed size for each image, and clips images that exceed frame bounds for neatness.</p><p>Great question for sure! Make sure when displaying a series of views in a and ensuring they all have the same dimensions so you do not need to set constraints.</p><p>Visit Apple Developer Forums here: <a href="https://developer.apple.com/forums/">https://developer.apple.com/forums/</a></p><div><hr></div><h2>&#127912; Colors &amp; Themes</h2><h3>How do create themes in a SwiftUI app. A theme that can hold typography, colors, spacing. How do we handle a case with multiple themes. And a case where the theme is fetched from the backend.</h3><p>You can create a file with all your application constants, and use conditional logic in the View to select which constant you need on a view by view basis, or pick up on environments such as light mode, dark mode or orientations.</p><p>If you need help, make a post on the forums!</p><div><hr></div><h3>If you use a non-semantic color how do you make sure it works in dark mode?</h3><p>I&#8217;d consider checking the colorScheme environment and using an appropriate color based on the current dark/light mode. See the <a href="https://developer.apple.com/documentation/swiftui/environmentvalues/colorscheme">following doc</a> for more details.</p><div><hr></div><h3>For designing is there a best practice for example the title in the trip detail is white but what if the photo is having a white color where the text is going to render, or there is a way to make it adaptable depending on the background to create the right contrast</h3><p>If the title sits on top of scrollable content, for example a toolbar or safe area bar, the color automatically adapts to the content beneath it.</p><div><hr></div><h3>Maho mentioned a neon color. Since it is not part of the system colors, how do we create it? Do we have to provide the light, dark and increased contrast versions?</h3><p>How to add a color literal to an Xcode project is discussed here: After adding a color to your asset catalogs, you can set up the color variants, and also set the hex value of the color. The Wishlist sample will be made available on the Apple developer website sometime after the event concludes. You can download it and look into the details of the color.</p><div><hr></div><h3>What impact does use of Color have on battery life? How can we override dark mode?</h3><p>Have you use the Instruments from Xcode3? I think like colors with higher saturation tend to require more brightness to be perceived clearly, which can drain a battery faster? But you can use the battery impact into the Instruments app to give you clear answers but using color and avoiding color. Remember that while overriding dark mode can help with visibility and comfort in bright environments, it may increase battery consumption but Instruments here are your best friend.</p><p>Visit Apple Developer Forums here: <a href="https://developer.apple.com/forums/">https://developer.apple.com/forums/</a></p><div><hr></div><h2>&#128202; Charts &amp; Performance</h2><h3>Following up on my Chart question. The chart is showing live data showing over time. I tested with instruments and tracked it down to the chart that is causing it. My current plan is to use Canvas instead for better performance but wanted to confirm I&#8217;m not missing something with Swift Charts</h3><p>If you have spotted out that Swift Charts is the performance bottleneck, yeah, drawing with Canvas with your own code may help, because that way you can only draw the content specific to your needs. But again, we will be interested in looking into more details about your use case. I suggest that you ask this question in our Developer Forums with your code snippets and analysis so SwiftUI folks can follow up from there. Thanks.</p><div><hr></div><h3>I have been trying to test Swift Charts with some sample tests. My use case requires updating the chart continually (at least a few times a second). For 200 data points I&#8217;m seeing around 40% CPU usage which is a lot for my use case where I might have multiple charts displaying at the same time.</h3><p>For a performance issue, I&#8217;d typically start with using Instruments to profile the app and figure out the performance bottleneck, and then see how it can be improved. The following doc may be worth reading: - Understanding and improving SwiftUI performance In your case, you might also consider if &#8220;at least a few times a second&#8221; is really needed and brings a lot of value. Refreshing a chart that frequently does consume resources.</p><div><hr></div><h2>&#128671; Platform Specific</h2><h3>I have a SwiftUI view which displays a List, with a few labels and an image. The list row height should be consistent for each item. When the list items grows large, the same code performs just fine on iOS, but starts lagging on macOS (26.2). What explains this?</h3><p>It is difficult to say for a specific app what would cause the difference. In general, first use Instruments to profile the app and optimize its performance. Check out &#8220;<a href="https://developer.apple.com/videos/play/wwdc2025/306/">Optimize SwiftUI performance with Instruments</a>&#8221;  for detailed information and tips. If you&#8217;re still having issues, please post on the Developer Forums for more assistance.</p><div><hr></div><h3>When building a native Mac app using SwiftUI how do I debug the title bar looking different on my machine (built via Xcode or exported) vs a testers machine even when on the same macOS version (26.2)? Ex: The title bar being transparent as expected in some places or just greyed at the top instead.</h3><p>Hello, you can troubleshoot visual inconsistencies across different devices by making sure all of your Appearance settings are the same between them, e.g. Dark Mode, Liquid Glass, Accessibility display settings, etc. Also make sure that the content underneath the titlebar is consistent. If you are still experiencing issues despite this (or the appearance is not what you would expect), please file a bug report via Feedback Assistant: <a href="https://developer.apple.com/bug-reporting/">https://developer.apple.com/bug-reporting/</a></p><div><hr></div><h3>What is the best way to make an app that works for all the devices (iPhone, iPad, Mac, TV, Watch, Vision Pro)? Making a single &#8220;empty&#8221; project in Xcode then adding each platform target, or a separate Xcode project per platform?</h3><p>Create a new Multiplatform app. Then in the target list in Xcode, add a new target; Add a Watch app that is a companion to the existing app target. In the Watch app target properties, check or uncheck the box for &#8220;Supports Running without iOS App Installation&#8221; depending on your app&#8217;s requirements.</p><div><hr></div><h3>In macOS, SwiftUI provides some default menus. How can I specify keyboard equivalents for default menu items without having to replace the implementation, specifically for Edit-&gt;Delete? Even if I provide an implementation, it seems I&#8217;m forced to replace all of the pasteboard commands, not just Delete.</h3><p>You can find a full guide on configuring the Mac menu bar here: <a href="https://developer.apple.com/documentation/swiftui/building-and-customizing-the-menu-bar-with-swiftui">https://developer.apple.com/documentation/swiftui/building-and-customizing-the-menu-bar-with-swiftui</a></p><div><hr></div><h3>Is SwiftUI ideal for extensions?</h3><p>Hello, if you&#8217;re interested in building app extensions with SwiftUI, it&#8217;s a snap when paired with WidgetKit!<br> <a href="https://developer.apple.com/documentation/swiftui/app-extensions">https://developer.apple.com/documentation/swiftui/app-extensions</a></p><div><hr></div><h3>Can SwiftUI be used to develop a 2D game? If not what is the suitable apple framework to use? (SwiftUI or SpriteKit or ARKit or something else?)</h3><p>SpriteKit is a framework you could use to add characters to your game. For setting up the game and its logic, I would start with GameKit! See this documentation to get started: <a href="https://developer.apple.com/documentation/GameKit/creating-real-time-games">https://developer.apple.com/documentation/GameKit/creating-real-time-games</a></p><p>And ARKit is if you want to make an immersive 3D app using the device camera.</p><div><hr></div><h3>Will the custom shape overlay MKMultiPolygon from UIKit MapKit be included in SwiftUI MapKit? It&#8217;s a major polygon type overlay for map engineering and developers currently have access to only Polygon,Circle,Polyline etc&#8230; any graceful suggestions in the mean time?</h3><p>Depending on how you&#8217;re currently using <code>MKMultiPolygon</code>, you could use a <code>ZStack</code> with multiple Polygons in your Map, and apply your style modifiers to the <code>ZStack</code> to apply them to all of the Polygons in the ZStack.</p><div><hr></div><h2>&#127929; TextField &amp; Keyboard</h2><h3>Is there any way to present an editable <code>TextField</code> that never presents a keyboard or responds to a USB keyboard? I&#8217;m building a calculator app with its own keypad that appends to an expression. But I&#8217;d like to give the user the ability to select parts of the expression or place the insertion point.</h3><p>SwiftUI doesn&#8217;t provide a way to replace the system-provided keyboard with an input view. I&#8217;d suggest that you file a feedback report for that. As a rescue, you can use <code>UITextField</code> + <code>UIViewRepresentable</code> (assuming iOS / iPadOS), and give the <code>UITextField</code> instance an inputView.</p><div><hr></div><h3><code>Textfield</code> - inline formatting. Currently, <code>Textfield</code> format API doesn&#8217;t support inline formatting . What is recommended way to format inline forms on swift ui text fields?</h3><p>I would start by looking at the prompt of a <code>TextField</code>, see: <a href="https://developer.apple.com/documentation/swiftui/textfield/init(_:value:format:prompt">https://developer.apple.com/documentation/swiftui/textfield/init(_:value:format:prompt</a>:)</p><div><hr></div><h2>&#128737;&#65039; Security</h2><h3>Where is the best place to keep Access Token and Refresh token/s handling most secure session for Swift Apps?</h3><p>Use the Keychain to store tokens and other secrets. See the <a href="https://developer.apple.com/documentation/security/keychain-items">API reference for Keychain items</a> for more information.</p><div><hr></div><h2>&#127950;&#65039; Concurrency</h2><h3>Say the code is already on Main queue, I used a check Thread.isMainThread and then dispatch on main if false, if true, execute as it is. I want to achieve similar behaviour using modern concurrency. Is it possible? Task {@MainActor} will always behave like main.async</h3><p>You can await <code>MainActor.run</code> to execute a synchronous piece of code on the MainActor, which should avoid suspending execution if you are already on the MainActor. If you need to know specifically <em>what actor you are on</em>, you can use the <code>#isolation</code> function argument to introspect the current execution context (which returns a type of <code>isolated (any Actor)?</code>). When you are nonisolated, this will be nil. Conversely, when you are isolated--and on the MainActor--it will be <code>.some(MainActor)</code>.</p><div><hr></div><h2>&#128396;&#65039; Icons &amp; App Design</h2><h3>Can I build an app icon from scratch in Icon Composer app or I&#8217;d need to first design the icon in Figma/Sketch first than import to Icon Composer to add liquid glass design?</h3><p>Hi, have you checked out the guide <a href="https://developer.apple.com/documentation/Xcode/creating-your-app-icon-using-icon-composer">Creating your app icon using Icon Composer</a>? You can download an App Icon Template to create the layers in Figma and Sketch, before importing to Icon Composer. Template available here: <a href="https://developer.apple.com/design/resources/">https://developer.apple.com/design/resources/</a></p><div><hr></div><h3>I am having trouble getting the new app icon process to work. My icons in the assets catalog work fine, but icons that I create in IconMaker do not appear at runtime after I add them to my project, whether iOS or macOS.</h3><p>Hi, you can review the Creating your app icon using Icon Composer article. It&#8217;s a great resource to learn about <a href="https://developer.apple.com/documentation/Xcode/creating-your-app-icon-using-icon-composer">using Icon Composer</a> and adding your Icon Composer file to your Xcode project.</p><div><hr></div><h2>&#127916; Toolbar &amp; Actions</h2><h3>Is there a heuristic for the number of toolbar actions to show before moving them into a &#8220;more&#8221; menu in the toolbar?</h3><p>You can&#8217;t predict the number of toolbar actions that can be shown before they will be displayed in a &#8220;more&#8221; menu. This will be highly dependent on the size of the device, the size of the window or view, and the size of the toolbar items.</p><div><hr></div><h3>On iPad, DocumentGroup&#8217;s title/rename UI gets clipped when I fill <code>.topBarLeading</code>, <code>.principal</code>, and <code>.topBarTrailing</code> (trailing is an HStack). Any recommended toolbar structure to preserve the system rename affordance without dropping <code>.principal</code> or using a menu?</h3><p>I think this would be a great question for the Developer Forums. I encourage you to post this question there so we can more easily share code and others with the same issue can find help for the same issue. Visit Apple Developer Forums here: <a href="https://developer.apple.com/forums/">https://developer.apple.com/forums/</a></p><div><hr></div><h3>Apple encourages us to use the navigation bar, but some Apple apps are starting to use more controls in the bottom of the screen, i.e. Safari. Should we focus on using the navigation bar or try to add buttons at the bottom. Thank you!</h3><p>I would encourage you to find what it works for you, recommendations are one thing, but we do not know your requirements, if the navigation bar fulfills your requirement</p><p>Your custom buttons have their merits and can be effective depending on the context and design goals of your app.</p><p>The navigation bar is a traditional element in iOS apps and is widely recognized by users. Typically occupies less screen space at the top, providing more room for content than buttons.</p><p>Consider how users naturally interact with the device. I don&#8217;t think this what you wanted to hear but personal requirements are hard to fulfill in one quick answer. I would recommend the forums if you have examples of what you trying to accomplish.</p><div><hr></div><h2>&#128311; Miscellaneous</h2><h3>Is swift UI good at managing memory within messages or keyboard extensions? Lots of bandwidth issues causing crashes when loading a lot of data. Host app is fine&#8230;anything helps!</h3><p>Extensions typically have a hard memory limit. The system doesn&#8217;t allow extensions to consume more memory than the limit. Simply switching to SwiftUI doesn&#8217;t change the limit, and probably doesn&#8217;t help reduce the memory consumption either. Memory consumption is typically related to data management, and so focusing on loading and using data more effectively may better help. I&#8217;d suggest that you ask this question in our Developer Forums with more details of how your extensions consumes memory, and we can have a deeper discussion from there. Thanks.</p><div><hr></div><h3>How to best abstract the model for views? A custom view like <code>SearchRow(tripName:, photoName:)</code> is already its own view model. But SwiftUI components like Button don&#8217;t give us access to their model. The scene would be like <code>ForEach(searchRows)</code> and <code>ForEach(buttons)</code> or more generally <code>ForEach(rows)</code></h3><p>Trying to understand your question and hope I got exactly what you mean with the best abstract model for a view as is hard to answer, I would recommend you to go to the Apple forums as well to follow up. When designing the SwiftUI views with models, for lists with interactive components like buttons, clearly separate and manage state effectively, a clear data model representing the information you display and interact with, view model holding instances of your data model and necessary business logic or state. Define custom views presentation logic for each item with state in the view model or pass closures to handle actions as your main view to iteratee over your view model&#8217;s data, passing state and actions to child views. Ensure state changes in interactive elements reflect in the view model, which updates the view due to properties.</p><div><hr></div><h3>I want to add a half screen mostly transparent dialog over the main window of a UI photo editing app. Examples or suggestions?</h3><p>I&#8217;d probably consider using ZStack to lay out a view above the main window and to the target position, make it almost transparent, and use it as the dialog.</p><div><hr></div><h3>In the example of <code>let _ = print()</code>, will that print get called every time the view updates or just on initial creation?</h3><p>It depends on where you put the <code>print</code>. If you put it at the beginning of the body, every time the SwiftUI recalculates the view body, the print will be called.</p><div><hr></div><h3>I use SwiftData and the @Query annotation to display my data. Each Item is rendered in a subview. Is there a way to pass the Item to be modifiable using either an annotation or something else? Let&#8217;s say a pinned attribute is being flipped and updating my @Query without an explicit call to SwiftData</h3><p>Not sure if I understand your question correctly, but if you intent is to pass a SwiftData model to a view and change the model in the subview, you can definitely do so, and <code>@Query</code> will automatically detect the changes on the model. If you are seeking a way to change an attribute of the model without calling the setter of the attribute, no, there is no way to do that.</p><div><hr></div><h3>Why are views discarded? The dependency tracking- is this feature&#8217;s efficiency directly dependent on the discarded views? The Apple Developer Library-the extent of the resources pulled from there?(Customizable?)</h3><p>SwiftUI efficiently manages memory by discarding views that are no longer visible or needed. This process optimizes performance by keeping only visible or recalculated views in memory. SwiftUI uses a declarative syntax to define the UI state, and the framework automatically updates views when the state changes. Instead of manually updating each view, SwiftUI only updates the affected parts of the view hierarchy. Views are part of a hierarchy, and when their state changes, SwiftUI recalculates the entire affected hierarchy. If a view is no longer visible or necessary, it is removed from memory, reducing memory usage and improving rendering performance.</p><p>There are numerous examples and code snippets demonstrating how to implement various features in SwiftUI and related technologies.</p><p>Yes, the efficiency of SwiftUI&#8217;s dependency tracking and rendering can be dependent on how effectively views are discarded. SwiftUI keeps track of which views depend on which other views</p><p>I don&#8217;t know what you want exactly from the library.</p><div><hr></div><h3>Is there a way to only draw a portion of a view at a time? With AppKit I&#8217;d use <code>-[NSView drawRect:]</code> to only draw visible parts of the view. I have a View whose body is a large Path, and I&#8217;d like to reduce the amount of drawing I need to do at once by only drawing the section of the Path visible.</h3><p>SwiftUI is a different approach, and the view body is updated as a whole based on changes to its data. You might be able to split this into multiple views that each represent part of the Path which can each be individually updated when the underlying data changes. The Developer Forums are a great resource to get more assistance with your specific use case.</p><div><hr></div><h2><strong>&#127942; </strong>Acknowledgments<strong> </strong></h2><p>A huge thank-you to everyone who joined in and shared thoughtful, insightful, and engaging questions throughout the session &#8212; your curiosity and input made the discussion truly rich and collaborative.</p><p><strong>Special thanks to:</strong></p><p>Aap, Aaron Brooks, Abdalla Mohammed, Alexis Schotte, Andrew Hall, Arpad Larrinaga Aviles, Ash, Ativ Patel, Brian Leighty, Cyril Fonrose, Dan Fabulich, Dan Stoian, Dan Zeitman, David Ko, David Mehedinti, David Veeneman, Dipl.-Ing. Wilfried Bergmann, Eleanor Spenceley, Elif Arifoglu, Elilan, Etkin, Evelyn, Farhana Mustafa, Fred Baker, Greg C, Ian Shimabukuro, James Clarke, Jan Armbrust, Jason Howlin, Jay Graham, Jesse Wesson, Jobie, John Ellenich, John Gethoefer, John Lawter, Jon Bailey, Jon Judelson, Jonas Helmer, Joshua Arnold, Julien, Kaan, Kaylee, Lloyd W Sykes, Lucy, Luis Rebollo, Maic Lopez Saenz, Maisomage, Malcolm Hall, Marin Joe Garcia, Markus, Matthew Brown, Michael Bachand, Michael K., mustii, Muthuveerappan Alagappan, Nathan Odendahl, Nicolas Dunning, Oliver Dieke, Omar zu&#241;iga, Oshua Arnold, Pete Hoch, Rayan Khan, Ricardo Moreno Martinez, Rick Mann, Rupinder kaur, Sam Wu, Satish Muthali, Siddhartha Khanna, Simon McLoughlin, SRINIVASA Potnuru, Tom Brodhurst-Hill, Tula W, Ulien, Vijay Ram, Walid Mohamed Nadim, Will Loew-Blosser, Willian Zhang, Yafes Duygulutuna, Yassin El Mouden, Yuriy Chekan and Zulfi Shah.</p><p>Now that&#8217;s a list! </p><p>Finally, a heartfelt thank-you to the <strong>Apple team and moderators</strong> for leading session, sharing expert guidance, and offering such clear explanations of the apps optimization techniques. Your contributions made this session an outstanding learning experience for everyone involved.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Swift Bits: Git Worktree]]></title><description><![CDATA[Branching the modern way]]></description><link>https://antongubarenko.substack.com/p/swift-bits-git-worktree</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swift-bits-git-worktree</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Tue, 10 Feb 2026 08:17:36 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/127ed1d7-d9c8-456b-ba0f-bf86a77e4b1b_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It&#8217;s funny how a tiny CLI command can shift your entire development workflow. For years I was stuck in the same pattern: git stash, git checkout, git stash pop, repeat and branching. Then I worked a lot with the ChatGPT macOS app and its connectors. Next, the Codex macOS app came along and, along with the Skills tab, there was a picker for the task run destination. Bottom bar is very informative:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!TUnZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!TUnZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png 424w, https://substackcdn.com/image/fetch/$s_!TUnZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png 848w, https://substackcdn.com/image/fetch/$s_!TUnZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png 1272w, https://substackcdn.com/image/fetch/$s_!TUnZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!TUnZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png" width="324" height="209.37184115523465" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:358,&quot;width&quot;:554,&quot;resizeWidth&quot;:324,&quot;bytes&quot;:37769,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/187137558?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!TUnZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png 424w, https://substackcdn.com/image/fetch/$s_!TUnZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png 848w, https://substackcdn.com/image/fetch/$s_!TUnZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png 1272w, https://substackcdn.com/image/fetch/$s_!TUnZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a><figcaption class="image-caption">Task destination in Codex macOS app</figcaption></figure></div><p>You can see a &#8220;New worktree&#8220; and that attracted my attention. But that was not the first time I had used it. I just didn&#8217;t know about it.</p><p>Claude app also use it for parallel session:</p><blockquote><h3>Work in parallel with sessions</h3><p>Click <strong>+ New session </strong>in the sidebar to work on multiple tasks in parallel. For Git repositories, each session gets its own isolated copy of your project using worktrees, so changes in one session don&#8217;t affect another until you commit them. Worktrees are stored in<code>~/.claude-worktrees/</code>by default.<br><a href="https://code.claude.com/docs/en/desktop">Docs</a></p></blockquote><p>And that&#8217;s why worktrees are hitting the spotlight lately because AI-powered tools (like <strong><a href="https://docs.conductor.build/?utm_source=chatgpt.com">Conductor</a>) </strong> and AI coding assistants: increasingly rely on running parallel workflows. And worktrees are the unsung hero that make those workflows fast and sane.</p><div><hr></div><h2>What Is Git Worktree, Really?</h2><p>Imagine this: you could check out <strong>multiple branches of the same repo at the same time</strong> without cloning, without stashing, and without jumping between contexts.</p><p>That&#8217;s exactly what <strong><a href="https://git-scm.com/docs/git-worktree">Git Worktrees</a></strong> let you do. Technically, worktrees create <strong>separate working directories</strong> that are all linked back to the same .git repository. You can have one directory on main, another on feature/foo, and another on hotfix/bar, all open at once. Each has its own working set of files, but they share the same Git history and object database &#8212; so commits, tags, and branches stay in sync without duplication.</p><p>It&#8217;s not magic! It&#8217;s Git&#8217;s native way of handling multiple working copies without wasting space. If you&#8217;ve ever cloned the same repo three times just to manage multitasking, consider this your apology from the universe.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mZ5S!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mZ5S!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!mZ5S!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!mZ5S!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!mZ5S!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mZ5S!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:383659,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/187137558?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!mZ5S!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!mZ5S!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!mZ5S!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!mZ5S!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Flow diagram</figcaption></figure></div><p>This approach also solve the old merging problem when you need to commit fast to the feature branch to jump back to <code>main/master</code>. Just make a worktree from the problematic branch and keep working.</p><p>Also, you might now that we can&#8217;t have multiple branches for a repo. Well, seems that we can do that. Not sure why you might need it? Next section will tell you more.</p><div><hr></div><h2>How You Can Use It (Beyond &#8220;Just Another Git Trick&#8221;)</h2><p>At first glance, worktrees are an efficiency hack for the multitasker. But their real superpower emerges in two increasingly common workflows:</p><h3>&#128736; Parallel Feature Development</h3><p>Instead of constantly switching context, you can open separate features in separate worktrees. Want to build a UI component in one and debug an urgent bug in another? Done.</p><blockquote><p>Our multiple branches issues from above</p></blockquote><h3>&#129302; AI-Powered Development</h3><p>With AI tools that spin up separate agents to run against your codebase (like Conductor does with <strong>parallel Claude Codes</strong>, each in its own sandboxed copy), worktrees make it possible to do this locally without cloning or spinning up separate VMs. Essentially, each agent gets a dedicated branch + working copy &#8212; without you having to manage massive clones.</p><p>This paradigm is quickly becoming <strong>the backbone of AI-augmented workflows</strong>: by avoiding stash chaos and enabling isolated contexts, worktrees let you let the machines do their thing without disrupting your flow.</p><div><hr></div><h2>A Simple Worktree Example: Create &amp; Remove</h2><p>Here&#8217;s the minimal set of commands you actually need. Think of these as your &#8220;git worktree starter pack&#8221;.</p><h3>Create a Worktree for a Feature</h3><pre><code># Create a new worktree for a feature branch
git worktree add ../feature-awesome -b feature/awesome</code></pre><p>What this does:</p><ul><li><p>Makes a new folder ../feature-awesome</p></li><li><p>Creates and checks out feature/awesome there</p></li><li><p>Links it back to your central .git repository</p><p>No stashing, no switching &#8212; everything stays in your current directory.</p></li></ul><h3>When You&#8217;re Done: Remove It</h3><pre><code>git worktree remove ../feature-awesome</code></pre><p>Boom &#8212; worktree gone. Git cleans up references, and the branch stays in your repo just like any other.</p><blockquote><p>&#128161;If you are keen to working with IDEs: Codex app have an task location picker at the bottom bar and Claude have a &#8220;New Session&#8220; which will all too use Worktree as we know now.</p></blockquote><div><hr></div><h2>Final Thoughts</h2><p>Git Worktree feels like one of those features that <em>s</em>hould have existed all along. It solves a real pain point: <strong>context switching chaos</strong>. And now that AI tools and agent managers are building their workflows on top of it, it&#8217;s fast becoming a tooling standard.</p><p>If you&#8217;re still in the mindset of git stash &#8594; git checkout &#8594; git stash pop, give Git Worktree a try. Your future self (and your future AI tools) will thank you.</p><p>Happy branching &#128640;!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h2>References</h2><ul><li><p><a href="https://git-scm.com/docs/git-worktree">Git worktree</a></p></li><li><p><a href="https://code.claude.com/docs/en/desktop">Claude Session Info</a></p></li><li><p><a href="https://developers.openai.com/codex/app/worktrees/">Codex Worktree Info</a></p></li></ul><p></p>]]></content:encoded></item><item><title><![CDATA[Swift Bits: Transition vs Transaction]]></title><description><![CDATA[Animate appearance and more!]]></description><link>https://antongubarenko.substack.com/p/swift-bits-transition-vs-transaction</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swift-bits-transition-vs-transaction</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Mon, 02 Feb 2026 07:01:32 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/0b4c8ebd-00e8-4219-ba26-fe61ef6af269_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>SwiftUI&#8217;s animation system is powerful but often misunderstood. Even Xcode, when you start typing a keyword, shows a load of almost identical options. What about when, in a tech interview, you are asked about the difference?</p><p>Two concepts that frequently come up when building polished, animated interfaces are <strong>Transitions</strong> and <strong>Transactions</strong>. They sound similar (and they&#8217;re both related to motion and state changes) but they serve very different purposes.</p><div><hr></div><h2>Transition</h2><p>A <strong><a href="https://developer.apple.com/documentation/swiftui/view/transition(_:)-5h5h0">Transition</a></strong> in SwiftUI defines <strong>how a view appears and disappears</strong> when it is inserted into or removed from the view hierarchy. It&#8217;s about <em>enter and exit animation</em> &#8212; the visual effect used when a view changes state and is added or removed.</p><p>Transitions don&#8217;t control timing or easing. They describe the <em>type of animation</em> that happens. The actual animation timing comes from a surrounding animation context.</p><h3>Built-in Transitions</h3><p>SwiftUI provides several built-in transitions:</p><pre><code>Text("Hello SwiftUI!")
    .transition(.opacity)</code></pre><p>You can also combine transitions:</p><pre><code>.transition(.scale.combined(with: .opacity))</code></pre><p>Or define asymmetric transitions, where insertion and removal behave differently:</p><pre><code>.transition(.asymmetric(
    insertion: .slide,
    removal: .scale
))</code></pre><p>Or create a custom Transition relying on <a href="https://developer.apple.com/documentation/swiftui/anytransition">AnyTransition</a> class. It&#8217;s pretty straightforward. How about blur and scale transition?<br>Need to make a transition modifier (yeah):</p><pre><code>struct BlurAndScaleTransition: ViewModifier {
    let progress: CGFloat

    func body(content: Content) -&gt; some View {
        content
            .scaleEffect(progress)
            .blur(radius: (1 - progress) * 10)
            .opacity(progress)
    }
}</code></pre><p>Then wrap in AnyTransition:</p><pre><code>extension AnyTransition {
    static var blurAndScale: AnyTransition {
        .modifier(
            active: BlurAndScaleTransition(progress: 0),
            identity: BlurAndScaleTransition(progress: 1)
        )
    }
}

// and use it! A plain example is shown in the next section
.transition(.blurAndScale)</code></pre><h3>How SwiftUI Applies Transitions</h3><p>Transitions take effect only when views are inserted into or removed from the view hierarchy. This usually happens inside conditional statements such as if blocks or when modifying collections in <code>List</code> or <code>ForEach</code>.</p><p>SwiftUI compares the previous and current view tree, detects insertions and removals, and applies the transition accordingly.</p><p>Importantly, <strong>a transition alone does not animate</strong>. It must be wrapped in an animation context using <code>withAnimation</code> or <code>.animation(_:)</code>.</p><h3>Example: Using a Transition</h3><pre><code>struct MyView: View {
    @State private var show = false

    var body: some View {
        VStack {
            if show {
                RoundedRectangle(cornerRadius: 20)
                    .fill(.blue)
                    .frame(height: 100)
                    .transition(.move(edge: .top))
            }

            Button("Toggle") {
                withAnimation(.easeInOut) {
                    show.toggle()
                }
            }
        }
    }
}</code></pre><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;86a56da0-e9a6-459c-b2e3-8139c70b93ca&quot;,&quot;duration&quot;:null}"></div><p>Here:</p><ul><li><p>The rectangle is inserted and removed from the hierarchy</p></li><li><p>The transition defines the movement</p></li><li><p>The animation defines timing and easing</p></li></ul><div><hr></div><h2>Transaction</h2><p>A <strong><a href="https://developer.apple.com/documentation/swiftui/transaction">Transaction</a></strong> in SwiftUI represents the <strong>context of a state change</strong>. It carries information about how SwiftUI should process that change, including animation behavior.</p><p>Every time SwiftUI processes a state update, it creates a Transaction and propagates it through the view hierarchy.</p><p>A transaction can include:</p><ul><li><p>The animation associated with the update</p></li><li><p>Whether animations are disabled</p></li><li><p>Metadata used internally by SwiftUI during rendering</p></li></ul><h3>Why Transactions Matter</h3><p>Transitions define <em>what</em> happens visually. Transactions define <em>how</em> that visual change is executed.</p><p>If you use <code>withAnimation</code>, SwiftUI attaches the animation to the transaction. That transaction then flows down to child views unless explicitly modified.</p><p>This mechanism explains why animations sometimes affect views you didn&#8217;t expect &#8212; they are all responding to the same transaction.</p><p>Let&#8217;s track what the transaction does in our example:</p><pre><code>struct SwiftBitsTransactionView: View {
    
    @State private var show = false
    
    var body: some View {
        VStack {
            if show {
                RoundedRectangle(cornerRadius: 20)
                    .fill(.blue)
                    .frame(height: 200)
                    .transition(.move(edge: .top))
                    //Tracking transaction for animation
                    .transaction { thx in
                        print(thx as Any)
                    }
            }
            
            Button("Toggle Rect") {
                withAnimation(.easeInOut) {
                    show.toggle()
                }
            }
        }
    }
}</code></pre><p>The console will reveal this:</p><pre><code>Transaction(plist: [TransactionPropertyKey&lt;AnimationKey&gt; = Optional(AnyAnimator(SwiftUI.BezierAnimation(duration: 0.35, curve: (extension in SwiftUI):SwiftUI.UnitCurve.CubicSolver(ax: 0.52, bx: -0.78, cx: 1.26, ay: -2.0, by: 3.0, cy: 0.0))))])</code></pre><p>And if we change it to Linear:</p><pre><code>Transaction(plist: [TransactionPropertyKey&lt;AnimationKey&gt; = Optional(AnyAnimator(SwiftUI.BezierAnimation(duration: 0.35, curve: (extension in SwiftUI):SwiftUI.UnitCurve.CubicSolver(ax: -2.0, bx: 3.0, cx: 0.0, ay: -2.0, by: 3.0, cy: 0.0))))])</code></pre><p>It really matches the result on screen. Nice!</p><h3>Modifying Transactions</h3><p>SwiftUI provides the <code>.transaction(_:) </code>modifier:</p><pre><code>Text("No animation")
    .transaction { thx in
        thx.animation = nil
    }</code></pre><p>this removes animation for that view and all of its children, even if the state change was wrapped in <code>withAnimation</code>.</p><p>Or, it can be used to override the original animation behavior:</p><pre><code>struct SwiftBitsTransactionView: View {
    
    @State private var show = false
    
    var body: some View {
        VStack {
            if show {
                RoundedRectangle(cornerRadius: 20)
                    .fill(.blue)
                    .frame(height: 200)
                    .transition(.move(edge: .top))
                    .transaction { thx in
                        thx.animation = thx
                            .animation?
                            .delay(2.0)
                            .speed(2)
                    }
            }
            
            Button("Toggle Rect") {
                withAnimation(.linear) {
                    show.toggle()
                }
            }
        }
    }
}</code></pre><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;54f8a87f-f2ad-4060-8636-9be4b3de5162&quot;,&quot;duration&quot;:null}"></div><h3>Important Transaction Properties</h3><ul><li><p><code>animation</code>: The animation applied to this update, if any</p></li><li><p><code>disablesAnimations</code>: A Boolean that disables animations for this subtree</p></li><li><p><code>addAnimationCompletion</code>: Completion closure to run when the animations created with this transaction are all complete</p></li></ul><p>Transactions allow very fine-grained control over animation behavior and are especially useful in complex view hierarchies.</p><div><hr></div><h2>Transition vs Transaction: Key Differences</h2><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!j0A9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!j0A9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png 424w, https://substackcdn.com/image/fetch/$s_!j0A9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png 848w, https://substackcdn.com/image/fetch/$s_!j0A9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png 1272w, https://substackcdn.com/image/fetch/$s_!j0A9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!j0A9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png" width="1456" height="192" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/db59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:192,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:42118,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/186294444?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!j0A9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png 424w, https://substackcdn.com/image/fetch/$s_!j0A9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png 848w, https://substackcdn.com/image/fetch/$s_!j0A9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png 1272w, https://substackcdn.com/image/fetch/$s_!j0A9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h3>Conceptual Difference</h3><ul><li><p><strong>Transitions</strong> answer: &#8220;How should this view appear or disappear?&#8221;</p></li><li><p><strong>Transactions</strong> answer: &#8220;How should this state change be animated?&#8221;</p></li></ul><p>Transitions are declarative and visual. Transactions are contextual and behavioral.</p><div><hr></div><h2>When to Use Which</h2><h3>Use Transitions When:</h3><ul><li><p>A view is conditionally shown or hidden</p></li><li><p>A view is inserted or removed from a collection</p></li><li><p>You want a clear visual effect for appearance or disappearance</p></li></ul><p>Examples include modals, banners, expanding panels, or list rows.</p><h3>Use Transactions When:</h3><ul><li><p>You need to override or suppress animations</p></li><li><p>You want different animation behavior in specific subtrees</p></li><li><p>You need programmatic control over animation propagation</p></li><li><p>Default animation behavior is too broad or unpredictable</p></li></ul><p>Transactions are more advanced and typically used when standard <code>.animation</code> and <code>.transition</code> modifiers are not enough.</p><p>Happy coding!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Swift Bits: CMMotionManager with Swift 6]]></title><description><![CDATA[Non-blocking device motion fetching]]></description><link>https://antongubarenko.substack.com/p/swift-bits-cmmotionmanager-with-swift</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swift-bits-cmmotionmanager-with-swift</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Tue, 20 Jan 2026 07:07:24 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/a166bebf-53d7-42d9-98ea-f62371b236a5_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Some journeys start with a first step. Others start with falling into a crash stack frame with placeholders&#8230;</p><p>For one of my apps, I was building a device motion tracking feature. Imagine a small circle-aligning game on a screen to prevent unnecessary action. Mostly all that was needed was to track <strong>pitch</strong> and <strong>roll</strong>. And from all the examples around the web, it&#8217;s pretty straightforward. But what is amazing about development jobs is how everything changes and you need to rewind and update the knowledge that you already have. No more guessing, a crash is waiting.</p><blockquote><p>I&#8217;ve spent quite some time to figure it out and apply a patch. Hope that it will save someone&#8217;s time.</p></blockquote><div><hr></div><h2>Tracking Motion</h2><p>To do this we need to use <code>CMMotionManager </code>(<a href="https://developer.apple.com/documentation/coremotion/cmmotionmanager">docs</a>) and a couple of methods:</p><ul><li><p><strong>startDeviceMotionUpdates</strong>: to get <strong>CMDeviceMotion</strong> with altitude, rotate rate and acceleration rate</p></li><li><p><strong>startAccelerometerUpdates</strong>: to get <strong>CMAccelerometerData</strong> with acceleration split by x/y/z axes</p></li><li><p><strong>startMagnetometerUpdates</strong>: to get <strong>CMMagnetometerData</strong> with magnetic field</p></li></ul><p>Each of the methods can be used with handlers (yes, it&#8217;s not migrated to async/await) or just plain call with further  <code>CMMotionManager</code> pooling. That is not very convenient and leads to usage of other method, for example:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!azQi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91ee8c34-12b2-4650-b007-ceba3df541a2_2688x1136.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!azQi!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91ee8c34-12b2-4650-b007-ceba3df541a2_2688x1136.png 424w, https://substackcdn.com/image/fetch/$s_!azQi!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91ee8c34-12b2-4650-b007-ceba3df541a2_2688x1136.png 848w, https://substackcdn.com/image/fetch/$s_!azQi!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91ee8c34-12b2-4650-b007-ceba3df541a2_2688x1136.png 1272w, https://substackcdn.com/image/fetch/$s_!azQi!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91ee8c34-12b2-4650-b007-ceba3df541a2_2688x1136.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!azQi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91ee8c34-12b2-4650-b007-ceba3df541a2_2688x1136.png" width="1456" height="615" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/91ee8c34-12b2-4650-b007-ceba3df541a2_2688x1136.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:615,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2570434,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/184523107?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91ee8c34-12b2-4650-b007-ceba3df541a2_2688x1136.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!azQi!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91ee8c34-12b2-4650-b007-ceba3df541a2_2688x1136.png 424w, https://substackcdn.com/image/fetch/$s_!azQi!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91ee8c34-12b2-4650-b007-ceba3df541a2_2688x1136.png 848w, https://substackcdn.com/image/fetch/$s_!azQi!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91ee8c34-12b2-4650-b007-ceba3df541a2_2688x1136.png 1272w, https://substackcdn.com/image/fetch/$s_!azQi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91ee8c34-12b2-4650-b007-ceba3df541a2_2688x1136.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Apple already suggests in the documentation:</p><blockquote><p><code>queue</code></p><p>An operation queue provided by the caller. Because the processed events might arrive at a high rate, using the main operation queue is <strong>not recommended</strong>.</p></blockquote><p>Indeed, don&#8217;t want to flood the Main Thread with sequenced and multiplied events. How it was done before Swift 6 and Strict Concurrency?</p><div><hr></div><h2>Swift 5</h2><p>It was a plain class with separate NSOperation. <code>ObservableObject</code> is used as common binding tool across the whole app (with <code>Observable</code> it wouldn&#8217;t have big performance increase since only 2 fields are updating). </p><pre><code><code>final class MotionManager: ObservableObject {
    private let manager = CMMotionManager()
    private let queue: OperationQueue = {
        let q = OperationQueue()
        q.name = "MotionManager.queue"
        q.qualityOfService = .userInitiated
        q.maxConcurrentOperationCount = 1
        return q
    }()
    
    private var isRunning = false    

    @Published var roll: Double = 0
    
    @Published var pitch: Double = 0
    
    func start() {
        guard !isRunning else {return}
        guard manager.isDeviceMotionAvailable else { return }
        manager.deviceMotionUpdateInterval = 1.0 / 60.0
        
        isRunning = true
        self.manager.startDeviceMotionUpdates(to: self.queue) { [weak self] motion, error in
            guard let motion else { return }
            DispatchQueue.main.async {
                self?.roll = motion.attitude.roll
                self?.pitch = motion.attitude.pitch
            }
        }
    }
    
    func stop() {
        isRunning = false
        manager.stopDeviceMotionUpdates()
    }

    
    deinit {
        stop()
    }
}</code></code></pre><div><hr></div><h2>Swift 6</h2><p>And this is where migration hits the isolation block. I&#8217;ve turned the <strong>Approachable Concurrency</strong> and set <strong>Default Isolation: MainActor</strong>. Concurrency Checkings: <strong>Full</strong>.</p><p>First of all, this refactoring was made:</p><pre><code>self.manager.startDeviceMotionUpdates(to: self.queue) { [weak self] motion, error in
    guard let motion else { return }
    let r = motion.attitude.roll
    let p = motion.attitude.pitch
    
    // This is a real MainActor hop
    Task { @MainActor [weak self] in
        self?.roll = r
        self?.pitch = p
    }
}</code></pre><p>No warnings, compiler check passed. Runtime is the next step. And now we are getting this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!R4Wm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54f30eb4-946a-4b68-a9e0-129f7d2ef55f_3680x3864.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!R4Wm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54f30eb4-946a-4b68-a9e0-129f7d2ef55f_3680x3864.png 424w, https://substackcdn.com/image/fetch/$s_!R4Wm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54f30eb4-946a-4b68-a9e0-129f7d2ef55f_3680x3864.png 848w, https://substackcdn.com/image/fetch/$s_!R4Wm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54f30eb4-946a-4b68-a9e0-129f7d2ef55f_3680x3864.png 1272w, https://substackcdn.com/image/fetch/$s_!R4Wm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54f30eb4-946a-4b68-a9e0-129f7d2ef55f_3680x3864.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!R4Wm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54f30eb4-946a-4b68-a9e0-129f7d2ef55f_3680x3864.png" width="1456" height="1529" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/54f30eb4-946a-4b68-a9e0-129f7d2ef55f_3680x3864.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1529,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:9357254,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/184523107?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54f30eb4-946a-4b68-a9e0-129f7d2ef55f_3680x3864.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!R4Wm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54f30eb4-946a-4b68-a9e0-129f7d2ef55f_3680x3864.png 424w, https://substackcdn.com/image/fetch/$s_!R4Wm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54f30eb4-946a-4b68-a9e0-129f7d2ef55f_3680x3864.png 848w, https://substackcdn.com/image/fetch/$s_!R4Wm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54f30eb4-946a-4b68-a9e0-129f7d2ef55f_3680x3864.png 1272w, https://substackcdn.com/image/fetch/$s_!R4Wm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F54f30eb4-946a-4b68-a9e0-129f7d2ef55f_3680x3864.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">First frame of the crash</figcaption></figure></div><p>Lovely, assembly commands with placeholders on non-Main thread. Other stacks are also informative:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8Afi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18ad6133-cc51-4c61-b9ba-503f024c0e79_3680x3072.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8Afi!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18ad6133-cc51-4c61-b9ba-503f024c0e79_3680x3072.png 424w, https://substackcdn.com/image/fetch/$s_!8Afi!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18ad6133-cc51-4c61-b9ba-503f024c0e79_3680x3072.png 848w, https://substackcdn.com/image/fetch/$s_!8Afi!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18ad6133-cc51-4c61-b9ba-503f024c0e79_3680x3072.png 1272w, https://substackcdn.com/image/fetch/$s_!8Afi!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18ad6133-cc51-4c61-b9ba-503f024c0e79_3680x3072.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8Afi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18ad6133-cc51-4c61-b9ba-503f024c0e79_3680x3072.png" width="1456" height="1215" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/18ad6133-cc51-4c61-b9ba-503f024c0e79_3680x3072.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1215,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:7760161,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/184523107?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18ad6133-cc51-4c61-b9ba-503f024c0e79_3680x3072.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!8Afi!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18ad6133-cc51-4c61-b9ba-503f024c0e79_3680x3072.png 424w, https://substackcdn.com/image/fetch/$s_!8Afi!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18ad6133-cc51-4c61-b9ba-503f024c0e79_3680x3072.png 848w, https://substackcdn.com/image/fetch/$s_!8Afi!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18ad6133-cc51-4c61-b9ba-503f024c0e79_3680x3072.png 1272w, https://substackcdn.com/image/fetch/$s_!8Afi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F18ad6133-cc51-4c61-b9ba-503f024c0e79_3680x3072.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Second frame trace</figcaption></figure></div><p>What do we see:</p><ul><li><p>LIBDISPATCH Assert: our CMMotionManager block expected to be on another thread.</p></li><li><p>Serial executor is checking that it matches the MainExecutor</p></li></ul><p>To be more sure, we can observe the iOS crash report on device (Organizer &#8594; Devices):</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!AdxH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d3ecc33-3ed8-4c21-a36a-f255516bbf8f_512x150.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AdxH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d3ecc33-3ed8-4c21-a36a-f255516bbf8f_512x150.png 424w, https://substackcdn.com/image/fetch/$s_!AdxH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d3ecc33-3ed8-4c21-a36a-f255516bbf8f_512x150.png 848w, https://substackcdn.com/image/fetch/$s_!AdxH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d3ecc33-3ed8-4c21-a36a-f255516bbf8f_512x150.png 1272w, https://substackcdn.com/image/fetch/$s_!AdxH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d3ecc33-3ed8-4c21-a36a-f255516bbf8f_512x150.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!AdxH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d3ecc33-3ed8-4c21-a36a-f255516bbf8f_512x150.png" width="512" height="150" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4d3ecc33-3ed8-4c21-a36a-f255516bbf8f_512x150.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:150,&quot;width&quot;:512,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:14906,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/184523107?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d3ecc33-3ed8-4c21-a36a-f255516bbf8f_512x150.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!AdxH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d3ecc33-3ed8-4c21-a36a-f255516bbf8f_512x150.png 424w, https://substackcdn.com/image/fetch/$s_!AdxH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d3ecc33-3ed8-4c21-a36a-f255516bbf8f_512x150.png 848w, https://substackcdn.com/image/fetch/$s_!AdxH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d3ecc33-3ed8-4c21-a36a-f255516bbf8f_512x150.png 1272w, https://substackcdn.com/image/fetch/$s_!AdxH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d3ecc33-3ed8-4c21-a36a-f255516bbf8f_512x150.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p> and it will reveal:</p><pre><code>ASI found [libdispatch.dylib] (sensitive) &#8216;BUG IN CLIENT OF LIBDISPATCH: Assertion failed: Block was expected to execute on queue [com.apple.main-thread (0x1fa25ca40)]&#8217;</code></pre><div><hr></div><h1>Solution</h1><p>We need to hop from MainActor to other executor. </p><p>First, need to keep the manager isolation and store <strong>pitch</strong> and roll <strong>handler</strong> values into constants to remove the <code>motion</code> races error.</p><pre><code>let r = motion.attitude.roll
let p = motion.attitude.pitch

//Sending 'motion' risks causing data races
Task { @MainActor [weak self] in
     self?.roll = r
     self?.pitch = p
}</code></pre><p>And finally, the best thing that comes to mind is to use the same <code>queue</code> to start <code>startDeviceMotionUpdates</code>. That will bring us to non-Main Actor and prevent crashes at runtime. </p><pre><code>final class MotionManager: ObservableObject {
    private let manager = CMMotionManager()
    private let queue: OperationQueue = {
        let q = OperationQueue()
        q.name = "MotionManager.queue"
        q.qualityOfService = .userInitiated
        q.maxConcurrentOperationCount = 1
        return q
    }()
    
    private var isRunning = false    

    @Published var roll: Double = 0
    
    @Published var pitch: Double = 0
    
    func start() {
        guard !isRunning else {return}
        guard manager.isDeviceMotionAvailable else { return }
        manager.deviceMotionUpdateInterval = 1.0 / 60.0
        
        isRunning = true
        queue.addOperation {
            self.manager.startDeviceMotionUpdates(to: self.queue) { [weak self] motion, error in
                guard let motion else { return }
                let r = motion.attitude.roll
                let p = motion.attitude.pitch
                
                // This is a real MainActor hop that the compiler understands.
                Task { @MainActor [weak self] in
                    self?.roll = r
                    self?.pitch = p
                }
            }
        }
    }
    
    func stop() {
        isRunning = false
        manager.stopDeviceMotionUpdates()
    }

    
    deinit {
        stop()
    }
}</code></pre><p>If you have a better idea, please write in the comments. Happy coding!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[Swift Bits: Menus]]></title><description><![CDATA[Context actions or filters]]></description><link>https://antongubarenko.substack.com/p/swift-bits-menus</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swift-bits-menus</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Tue, 06 Jan 2026 08:49:01 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/31c2d4c8-6b0a-413f-bb45-5bcc71d2c95c_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Menus in iOS are no longer just contextual pop-ups hidden behind long presses. Starting from iOS 14, Apple introduced a unified Menu system that works across buttons, navigation bars, toolbars, and context interactions.</p><p>Recently, for one of the apps (built with UIKit, with some parts in SwiftUI), there was a task to add filtering. Typically, a separate sheet or modal view works better &#8212; filter state handling, restoration, and persistence are more flexible with that approach. However, basic two-option sorting can be implemented inline as well.</p><div><hr></div><h2>What is a Menu in iOS?</h2><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!My-i!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6420dab-9050-4864-a5f3-3b6622672685_448x224.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!My-i!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6420dab-9050-4864-a5f3-3b6622672685_448x224.png 424w, https://substackcdn.com/image/fetch/$s_!My-i!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6420dab-9050-4864-a5f3-3b6622672685_448x224.png 848w, https://substackcdn.com/image/fetch/$s_!My-i!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6420dab-9050-4864-a5f3-3b6622672685_448x224.png 1272w, https://substackcdn.com/image/fetch/$s_!My-i!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6420dab-9050-4864-a5f3-3b6622672685_448x224.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!My-i!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6420dab-9050-4864-a5f3-3b6622672685_448x224.png" width="338" height="169" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a6420dab-9050-4864-a5f3-3b6622672685_448x224.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:224,&quot;width&quot;:448,&quot;resizeWidth&quot;:338,&quot;bytes&quot;:23350,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/182788083?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6420dab-9050-4864-a5f3-3b6622672685_448x224.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!My-i!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6420dab-9050-4864-a5f3-3b6622672685_448x224.png 424w, https://substackcdn.com/image/fetch/$s_!My-i!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6420dab-9050-4864-a5f3-3b6622672685_448x224.png 848w, https://substackcdn.com/image/fetch/$s_!My-i!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6420dab-9050-4864-a5f3-3b6622672685_448x224.png 1272w, https://substackcdn.com/image/fetch/$s_!My-i!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa6420dab-9050-4864-a5f3-3b6622672685_448x224.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><p>A Menu is a structured collection of actions presented to the user in a compact, system-styled interface. Unlike alerts or action sheets, menus are:</p><ul><li><p>Lightweight</p></li><li><p>Non-blocking</p></li><li><p>Context-aware</p></li><li><p>Pointer- and keyboard-friendly</p></li></ul><p>Menus are backed by the different core building blocks across UIKit and SwiftUI:</p><ul><li><p><a href="https://developer.apple.com/documentation/uikit/uimenu">UIMenu</a> for UIKit</p></li><li><p><a href="https://developer.apple.com/documentation/uikit/uiaction">UIAction</a> for UIKit</p></li><li><p><a href="https://developer.apple.com/documentation/swiftui/menu">Menu</a> for SwiftUI</p></li></ul><div><hr></div><h2>When Should You Use a Menu?</h2><p>Menus are ideal when multiple related actions exist, but none of them is primary enough to deserve its own button.</p><h3>&#9989; Good use cases</h3><ul><li><p>Sorting &amp; filtering</p></li><li><p>Display mode selection</p></li><li><p>Secondary actions (Share, Duplicate, Rename)</p></li><li><p>Overflow actions (&#8226;&#8226;&#8226; buttons)</p></li><li><p>Toolbar or navigation bar actions</p></li></ul><p>Examples:</p><ul><li><p>&#8220;Sort by Name / Date / Recommended&#8221;</p></li><li><p>&#8220;View as List / Grid&#8221;</p></li><li><p>&#8220;More&#8221; button in navigation bars</p></li></ul><h3>&#10060; Avoid menus when:</h3><ul><li><p>There is a single primary action</p></li><li><p>Actions are time-critical</p></li><li><p>User must discover the action immediately</p></li><li><p>The menu hides destructive actions too deeply</p></li></ul><blockquote><p>If the user has to <em>guess</em> where an action is &#8212; don&#8217;t put it in a menu.</p></blockquote><div><hr></div><h2>Apple Human Interface Guidelines (HIG)</h2><p>Such a UI-ish thing could not be missed by Apple Designers. We also should not skip the recommendations. Take your time to check them out: <a href="https://developer.apple.com/design/human-interface-guidelines/menus">Human Interface Guidelines</a>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!YvCn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd77b1ed3-c8a2-440d-bc64-39f24af10dfa_1480x832.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YvCn!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd77b1ed3-c8a2-440d-bc64-39f24af10dfa_1480x832.png 424w, https://substackcdn.com/image/fetch/$s_!YvCn!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd77b1ed3-c8a2-440d-bc64-39f24af10dfa_1480x832.png 848w, https://substackcdn.com/image/fetch/$s_!YvCn!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd77b1ed3-c8a2-440d-bc64-39f24af10dfa_1480x832.png 1272w, https://substackcdn.com/image/fetch/$s_!YvCn!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd77b1ed3-c8a2-440d-bc64-39f24af10dfa_1480x832.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!YvCn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd77b1ed3-c8a2-440d-bc64-39f24af10dfa_1480x832.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d77b1ed3-c8a2-440d-bc64-39f24af10dfa_1480x832.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;A stylized representation of a menu containing a selected item and displaying a submenu. The image is tinted red to subtly reflect the red in the original six-color Apple logo.&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A stylized representation of a menu containing a selected item and displaying a submenu. The image is tinted red to subtly reflect the red in the original six-color Apple logo." title="A stylized representation of a menu containing a selected item and displaying a submenu. The image is tinted red to subtly reflect the red in the original six-color Apple logo." srcset="https://substackcdn.com/image/fetch/$s_!YvCn!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd77b1ed3-c8a2-440d-bc64-39f24af10dfa_1480x832.png 424w, https://substackcdn.com/image/fetch/$s_!YvCn!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd77b1ed3-c8a2-440d-bc64-39f24af10dfa_1480x832.png 848w, https://substackcdn.com/image/fetch/$s_!YvCn!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd77b1ed3-c8a2-440d-bc64-39f24af10dfa_1480x832.png 1272w, https://substackcdn.com/image/fetch/$s_!YvCn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd77b1ed3-c8a2-440d-bc64-39f24af10dfa_1480x832.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Courtesy of Apple Inc.</figcaption></figure></div><p>Key recommendations from Apple:</p><ul><li><p>Keep menus short and scannable</p></li><li><p>Group related actions</p></li><li><p>Put destructive actions last</p></li><li><p>Use SF Symbols consistently</p></li><li><p>Avoid deeply nested menus</p></li></ul><p>Menus should feel <em>predictable</em>, not clever. Time to create some!</p><div><hr></div><h2>Adding a Menu in UIKit</h2><p>UIKit&#8217;s menu system is powerful and works almost everywhere: buttons, bar items, and even views.</p><h3>Menu on a UIButton</h3><pre><code>enum SortOption {
    case name
    case date
}

var selectedSort: SortOption = .name

func makeSortMenu() -&gt; UIMenu {
    let sortByName = UIAction(
        title: "Sort by Name",
        state: selectedSort == .name ? .on : .off
    ) { _ in
        selectedSort = .name
        updateMenu()
    }

    let sortByDate = UIAction(
        title: "Sort by Date",
        state: selectedSort == .date ? .on : .off
    ) { _ in
        selectedSort = .date
        updateMenu()
    }

    return UIMenu(title: "Sort", children: [sortByName, sortByDate])
}


let button = UIButton(type: .system)
button.setImage(UIImage(systemName: "arrow.up.arrow.down"), for: .normal)
button.menu = makeSortMenu()
//Don't forget this! Or menu will not show up instantly.
button.showsMenuAsPrimaryAction = true</code></pre><h3>Menu in Navigation Bar</h3><pre><code>navigationItem.rightBarButtonItem = UIBarButtonItem(
    title: nil,
    image: UIImage(systemName: "ellipsis.circle"),
    primaryAction: nil,
    menu: menu
)</code></pre><p>This pattern replaces custom action sheets and is now Apple&#8217;s preferred solution.</p><div><hr></div><h2>Adding a Menu in SwiftUI</h2><p>SwiftUI introduces a dedicated Menu view. It&#8217;s easy to add it like any other View. In example:</p><h3>Basic Menu</h3><pre><code>enum SortOption {
    case name
    case date
}

@State private var selectedSort: SortOption = .name

var body: some View {
    Menu {
        Button {
            selectedSort = .name
        } label: {
            Label(
                "Sort by Name",
                systemImage: selectedSort == .name ? "checkmark" : ""
            )
        }

        Button {
            selectedSort = .date
        } label: {
            Label(
                "Sort by Date",
                systemImage: selectedSort == .date ? "checkmark" : ""
            )
        }
    } label: {
        Label("Sort", systemImage: "arrow.up.arrow.down")
    }
}</code></pre><p>If you want a more scalable approach:</p><pre><code>Menu {
    Picker("Sort", selection: $selectedSort) {
        Text("Sort by Name").tag(SortOption.name)
        Text("Sort by Date").tag(SortOption.date)
    }
} label: {
    Label("Sort", systemImage: "arrow.up.arrow.down")
}</code></pre><h3>Menu in Toolbars</h3><pre><code>.toolbar {
    Menu {
        Button("Refresh") {}
        Button("Settings") {}
    } label: {
        Image(systemName: "ellipsis.circle")
    }
}</code></pre><h3>Submenus</h3><p>We can easily add a submenu. Just keep in mind:</p><ul><li><p>Submenus should not be too different from other menu items meaning</p></li><li><p>Don&#8217;t overuse the logic. Submenu in submenu is to much. It&#8217;s not <a href="https://westcoastcustoms.com">West Coast Customs</a> branch of &#8220;<a href="https://en.wikipedia.org/wiki/Pimp_My_Ride">Pimp My Ride</a>&#8220; )</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!s-ea!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85dcb34e-a36a-4b45-8d76-c1c56ef3da8c_412x412.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!s-ea!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85dcb34e-a36a-4b45-8d76-c1c56ef3da8c_412x412.png 424w, https://substackcdn.com/image/fetch/$s_!s-ea!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85dcb34e-a36a-4b45-8d76-c1c56ef3da8c_412x412.png 848w, https://substackcdn.com/image/fetch/$s_!s-ea!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85dcb34e-a36a-4b45-8d76-c1c56ef3da8c_412x412.png 1272w, https://substackcdn.com/image/fetch/$s_!s-ea!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85dcb34e-a36a-4b45-8d76-c1c56ef3da8c_412x412.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!s-ea!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85dcb34e-a36a-4b45-8d76-c1c56ef3da8c_412x412.png" width="288" height="288" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/85dcb34e-a36a-4b45-8d76-c1c56ef3da8c_412x412.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:412,&quot;width&quot;:412,&quot;resizeWidth&quot;:288,&quot;bytes&quot;:45080,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/182788083?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85dcb34e-a36a-4b45-8d76-c1c56ef3da8c_412x412.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!s-ea!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85dcb34e-a36a-4b45-8d76-c1c56ef3da8c_412x412.png 424w, https://substackcdn.com/image/fetch/$s_!s-ea!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85dcb34e-a36a-4b45-8d76-c1c56ef3da8c_412x412.png 848w, https://substackcdn.com/image/fetch/$s_!s-ea!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85dcb34e-a36a-4b45-8d76-c1c56ef3da8c_412x412.png 1272w, https://substackcdn.com/image/fetch/$s_!s-ea!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85dcb34e-a36a-4b45-8d76-c1c56ef3da8c_412x412.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Menu un Submenu&#8230;</figcaption></figure></div></li><li><p>Menu content hierarchy for Submenus is reversed. If you want to have it below other option &#8594; put it on top like in example.</p></li></ul><pre><code><code>enum SortOption {
    case name
    case date
}

enum SortDirection {
    case asc
    case desc
}

@State private var selectedSort: SortOption = .name
@State private var selectedSortDirection: SortDirection = .asc

Menu {
    Menu {
        Picker("Direction", selection: $selectedSortDirection) {
            Text("Ascending").tag(SortDirection.asc)
            Text("Descending").tag(SortDirection.desc)
        }
    } label: {
        Label("Direction", systemImage: "arrow.up.arrow.down")
    }
    
    Picker("Sort", selection: $selectedSort) {
        Text("Sort by Name").tag(SortOption.name)
        Text("Sort by Date").tag(SortOption.date)
    }
} label: {
    Label("Sort", systemImage: "arrow.up.arrow.down")
}</code></code></pre><div><hr></div><h2>Final Thoughts</h2><p>Menus are no longer a secondary UI element in iOS &#8212; they&#8217;re a core interaction pattern.</p><p>They:</p><ul><li><p>Replace action sheets</p></li><li><p>Reduce UI clutter</p></li><li><p>Scale naturally from iPhone to iPad and Mac</p></li></ul><p>If your app still relies on custom &#8220;More&#8221; sheets or overloaded toolbars, it&#8217;s probably time to switch to menus.</p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Swift Bits: Override Color Scheme]]></title><description><![CDATA[Change theme straight from app]]></description><link>https://antongubarenko.substack.com/p/swift-bits-override-color-scheme</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swift-bits-override-color-scheme</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Tue, 30 Dec 2025 10:05:28 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/3dcc53f9-31cc-4bb2-a6e8-b5cb3ab6fc02_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Light and Dark Theme is one of the ancient holy-war topics in development. Our predecessors were sitting near a campfire (Dark Souls fans - I salute you), setting screensavers on CRT monitors&#8230; </p><p>In iOS, it hasn&#8217;t been around for that long, but still many apps lack proper support of it. This raises a big question: should an app adapt to system preferences and make a consistent UX for device? But for now, let&#8217;s just learn how to change it &#8212; or not.</p><h2>When was Dark/Light Mode introduced in iOS?</h2><p>Modes was officially introduced in<strong> iOS 13 (2019).</strong></p><p>Before iOS 13, developers could only simulate &#8220;dark themes&#8221; manually by:</p><ul><li><p>Using custom color palettes</p></li><li><p>Detecting the time of day (yeah&#8230; kind of a solution, to be honest)</p></li></ul><p>With <strong>iOS 13</strong>, Apple introduced:</p><ul><li><p>System-wide Light &amp; Dark appearance</p></li><li><p>Dynamic colors</p></li><li><p>Asset Catalog appearance variants</p></li></ul><p>Now we can both read and change the color scheme. The values are:</p><ul><li><p>.light</p></li><li><p>.dark</p></li><li><p>.unspecified (system default)</p></li></ul><p>They are self-explanatory and we can observe and change them. Differently for each framework.</p><div><hr></div><h2>UIKit</h2><p>Many apps are still written in UIKit, no doubtsI initially found a nice color scheme solution in a UIKit-based app.</p><h3>Read</h3><p>UIKit notifies views and view controllers when traits change:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!gyIi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe577fb6c-3d0e-4a1c-975a-78f446f88ccb_3680x1576.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!gyIi!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe577fb6c-3d0e-4a1c-975a-78f446f88ccb_3680x1576.png 424w, https://substackcdn.com/image/fetch/$s_!gyIi!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe577fb6c-3d0e-4a1c-975a-78f446f88ccb_3680x1576.png 848w, https://substackcdn.com/image/fetch/$s_!gyIi!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe577fb6c-3d0e-4a1c-975a-78f446f88ccb_3680x1576.png 1272w, https://substackcdn.com/image/fetch/$s_!gyIi!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe577fb6c-3d0e-4a1c-975a-78f446f88ccb_3680x1576.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!gyIi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe577fb6c-3d0e-4a1c-975a-78f446f88ccb_3680x1576.png" width="724" height="310.2857142857143" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e577fb6c-3d0e-4a1c-975a-78f446f88ccb_3680x1576.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:624,&quot;width&quot;:1456,&quot;resizeWidth&quot;:724,&quot;bytes&quot;:4294960,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/182788128?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe577fb6c-3d0e-4a1c-975a-78f446f88ccb_3680x1576.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!gyIi!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe577fb6c-3d0e-4a1c-975a-78f446f88ccb_3680x1576.png 424w, https://substackcdn.com/image/fetch/$s_!gyIi!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe577fb6c-3d0e-4a1c-975a-78f446f88ccb_3680x1576.png 848w, https://substackcdn.com/image/fetch/$s_!gyIi!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe577fb6c-3d0e-4a1c-975a-78f446f88ccb_3680x1576.png 1272w, https://substackcdn.com/image/fetch/$s_!gyIi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe577fb6c-3d0e-4a1c-975a-78f446f88ccb_3680x1576.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>If you need to access it:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!JeYV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe79b95-bdbf-4a7c-90da-4821873dcaad_3560x1400.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JeYV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe79b95-bdbf-4a7c-90da-4821873dcaad_3560x1400.png 424w, https://substackcdn.com/image/fetch/$s_!JeYV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe79b95-bdbf-4a7c-90da-4821873dcaad_3560x1400.png 848w, https://substackcdn.com/image/fetch/$s_!JeYV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe79b95-bdbf-4a7c-90da-4821873dcaad_3560x1400.png 1272w, https://substackcdn.com/image/fetch/$s_!JeYV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe79b95-bdbf-4a7c-90da-4821873dcaad_3560x1400.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!JeYV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe79b95-bdbf-4a7c-90da-4821873dcaad_3560x1400.png" width="1456" height="573" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bfe79b95-bdbf-4a7c-90da-4821873dcaad_3560x1400.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:573,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:3792031,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/182788128?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe79b95-bdbf-4a7c-90da-4821873dcaad_3560x1400.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!JeYV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe79b95-bdbf-4a7c-90da-4821873dcaad_3560x1400.png 424w, https://substackcdn.com/image/fetch/$s_!JeYV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe79b95-bdbf-4a7c-90da-4821873dcaad_3560x1400.png 848w, https://substackcdn.com/image/fetch/$s_!JeYV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe79b95-bdbf-4a7c-90da-4821873dcaad_3560x1400.png 1272w, https://substackcdn.com/image/fetch/$s_!JeYV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe79b95-bdbf-4a7c-90da-4821873dcaad_3560x1400.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>UIView also holds the same traits collection but doesn&#8217;t allow to override it.</p><pre><code><strong>@property</strong> (<strong>nonatomic</strong>, <strong>readonly</strong>) UIUserInterfaceStyle userInterfaceStyle API_AVAILABLE(tvos(10.0)) API_AVAILABLE(ios(12.0)) API_UNAVAILABLE(watchos);</code></pre><h3>Set</h3><p>For <code>UIController</code>, we can override for the whole controller:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-0Hx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59eb86ba-50f3-4680-b181-ee76a4a2dc6e_2080x1136.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-0Hx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59eb86ba-50f3-4680-b181-ee76a4a2dc6e_2080x1136.png 424w, https://substackcdn.com/image/fetch/$s_!-0Hx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59eb86ba-50f3-4680-b181-ee76a4a2dc6e_2080x1136.png 848w, https://substackcdn.com/image/fetch/$s_!-0Hx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59eb86ba-50f3-4680-b181-ee76a4a2dc6e_2080x1136.png 1272w, https://substackcdn.com/image/fetch/$s_!-0Hx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59eb86ba-50f3-4680-b181-ee76a4a2dc6e_2080x1136.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-0Hx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59eb86ba-50f3-4680-b181-ee76a4a2dc6e_2080x1136.png" width="516" height="281.7445054945055" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/59eb86ba-50f3-4680-b181-ee76a4a2dc6e_2080x1136.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:795,&quot;width&quot;:1456,&quot;resizeWidth&quot;:516,&quot;bytes&quot;:2012638,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/182788128?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59eb86ba-50f3-4680-b181-ee76a4a2dc6e_2080x1136.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!-0Hx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59eb86ba-50f3-4680-b181-ee76a4a2dc6e_2080x1136.png 424w, https://substackcdn.com/image/fetch/$s_!-0Hx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59eb86ba-50f3-4680-b181-ee76a4a2dc6e_2080x1136.png 848w, https://substackcdn.com/image/fetch/$s_!-0Hx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59eb86ba-50f3-4680-b181-ee76a4a2dc6e_2080x1136.png 1272w, https://substackcdn.com/image/fetch/$s_!-0Hx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59eb86ba-50f3-4680-b181-ee76a4a2dc6e_2080x1136.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div><hr></div><h2>SwiftUI</h2><p>And this is a modern UI approach. We have an environment variable <code>colorScheme</code>. You can read more about it in <a href="https://developer.apple.com/documentation/swiftui/colorscheme">Apple Docs</a>. </p><h3>Read</h3><p>SwiftUI exposes appearance like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!U5bd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F633425b1-f1c6-43c8-8dc7-a9ad93266aeb_2504x1224.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!U5bd!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F633425b1-f1c6-43c8-8dc7-a9ad93266aeb_2504x1224.png 424w, https://substackcdn.com/image/fetch/$s_!U5bd!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F633425b1-f1c6-43c8-8dc7-a9ad93266aeb_2504x1224.png 848w, https://substackcdn.com/image/fetch/$s_!U5bd!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F633425b1-f1c6-43c8-8dc7-a9ad93266aeb_2504x1224.png 1272w, https://substackcdn.com/image/fetch/$s_!U5bd!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F633425b1-f1c6-43c8-8dc7-a9ad93266aeb_2504x1224.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!U5bd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F633425b1-f1c6-43c8-8dc7-a9ad93266aeb_2504x1224.png" width="516" height="252.32967032967034" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/633425b1-f1c6-43c8-8dc7-a9ad93266aeb_2504x1224.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:712,&quot;width&quot;:1456,&quot;resizeWidth&quot;:516,&quot;bytes&quot;:2481239,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/182788128?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F633425b1-f1c6-43c8-8dc7-a9ad93266aeb_2504x1224.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!U5bd!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F633425b1-f1c6-43c8-8dc7-a9ad93266aeb_2504x1224.png 424w, https://substackcdn.com/image/fetch/$s_!U5bd!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F633425b1-f1c6-43c8-8dc7-a9ad93266aeb_2504x1224.png 848w, https://substackcdn.com/image/fetch/$s_!U5bd!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F633425b1-f1c6-43c8-8dc7-a9ad93266aeb_2504x1224.png 1272w, https://substackcdn.com/image/fetch/$s_!U5bd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F633425b1-f1c6-43c8-8dc7-a9ad93266aeb_2504x1224.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Set</h3><p>You can override the user&#8217;s system appearance for a specific view hierarchy by applying the <code>preferredColorScheme(_:)</code> view modifier.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lZNm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fe89ef7-73df-40d4-96f2-679218505397_2248x1576.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lZNm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fe89ef7-73df-40d4-96f2-679218505397_2248x1576.png 424w, https://substackcdn.com/image/fetch/$s_!lZNm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fe89ef7-73df-40d4-96f2-679218505397_2248x1576.png 848w, https://substackcdn.com/image/fetch/$s_!lZNm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fe89ef7-73df-40d4-96f2-679218505397_2248x1576.png 1272w, https://substackcdn.com/image/fetch/$s_!lZNm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fe89ef7-73df-40d4-96f2-679218505397_2248x1576.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lZNm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fe89ef7-73df-40d4-96f2-679218505397_2248x1576.png" width="514" height="360.43543956043953" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2fe89ef7-73df-40d4-96f2-679218505397_2248x1576.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1021,&quot;width&quot;:1456,&quot;resizeWidth&quot;:514,&quot;bytes&quot;:2764819,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/182788128?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fe89ef7-73df-40d4-96f2-679218505397_2248x1576.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!lZNm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fe89ef7-73df-40d4-96f2-679218505397_2248x1576.png 424w, https://substackcdn.com/image/fetch/$s_!lZNm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fe89ef7-73df-40d4-96f2-679218505397_2248x1576.png 848w, https://substackcdn.com/image/fetch/$s_!lZNm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fe89ef7-73df-40d4-96f2-679218505397_2248x1576.png 1272w, https://substackcdn.com/image/fetch/$s_!lZNm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fe89ef7-73df-40d4-96f2-679218505397_2248x1576.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div><hr></div><h2>Overriding Light / Dark Mode</h2><p>By default, iOS determines an app&#8217;s appearance based on the system-wide setting chosen by the user. This setting propagates through the entire UI hierarchy automatically and consistently.</p><p>However, iOS also allows developers to override this behavior. An override lets your app (or a specific part of it) request a different appearance than the system preference.</p><p>This capability exists for intentional, contextual use cases, not for bypassing user choice without reason.</p><h3>Overriding vs. Theming (Important Distinction)</h3><p>Overriding appearance is <strong>not the same</strong> as building a theme system.</p><ul><li><p>Appearance override &#8594; <em>Light / Dark only</em></p></li><li><p>Theming &#8594; colors, typography, spacing, branding</p></li></ul><p>You should never use Light/Dark overrides to implement:</p><ul><li><p>Brand colors</p></li><li><p>Feature-based themes</p></li><li><p>Seasonal designs</p></li></ul><p>Those belong in a <strong>design system</strong>, not in appearance traits.</p><p>If you still decide to proceed - there are <strong>two levels of override</strong>:</p><ol><li><p>App-wide (global)</p></li><li><p>User-controlled (stored in UserDefaults, in example)</p></li></ol><h3>Global override (force appearance)</h3><p>An <strong>app-wide override</strong> forces a single appearance for the entire application, regardless of the user&#8217;s system preference.</p><p>This override is applied at the <strong>highest possible level</strong> of the UI hierarchy &#8212; typically the UIWindow (UIKit) or the root view of the app (SwiftUI). From that point downward, every screen, component, and child view inherits the same appearance.</p><p>What does this mean in practice?</p><ul><li><p>The entire app is locked to Light or Dark</p></li><li><p>No screen can opt out unless it explicitly overrides again</p></li><li><p>Dynamic system colors resolve consistently across all UI</p></li><li><p>The override behaves like a <em>global environment rule</em></p></li></ul><h3>UIKit / SwiftUI</h3><p>This snippet is already adapted to latest Scene windows handling (works for both frameworks):</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nu7j!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe46605b8-b218-4803-9f49-1ac914e57b31_2868x1752.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nu7j!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe46605b8-b218-4803-9f49-1ac914e57b31_2868x1752.png 424w, https://substackcdn.com/image/fetch/$s_!nu7j!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe46605b8-b218-4803-9f49-1ac914e57b31_2868x1752.png 848w, https://substackcdn.com/image/fetch/$s_!nu7j!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe46605b8-b218-4803-9f49-1ac914e57b31_2868x1752.png 1272w, https://substackcdn.com/image/fetch/$s_!nu7j!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe46605b8-b218-4803-9f49-1ac914e57b31_2868x1752.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nu7j!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe46605b8-b218-4803-9f49-1ac914e57b31_2868x1752.png" width="648" height="395.65384615384613" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e46605b8-b218-4803-9f49-1ac914e57b31_2868x1752.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:889,&quot;width&quot;:1456,&quot;resizeWidth&quot;:648,&quot;bytes&quot;:3778804,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/182788128?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe46605b8-b218-4803-9f49-1ac914e57b31_2868x1752.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!nu7j!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe46605b8-b218-4803-9f49-1ac914e57b31_2868x1752.png 424w, https://substackcdn.com/image/fetch/$s_!nu7j!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe46605b8-b218-4803-9f49-1ac914e57b31_2868x1752.png 848w, https://substackcdn.com/image/fetch/$s_!nu7j!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe46605b8-b218-4803-9f49-1ac914e57b31_2868x1752.png 1272w, https://substackcdn.com/image/fetch/$s_!nu7j!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe46605b8-b218-4803-9f49-1ac914e57b31_2868x1752.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>or hold an <code>Observable</code> in the root of your app and change <code>preferredColorScheme</code>.</p><div><hr></div><h3> User-controlled</h3><p>This approach is intended for targeted UI tweaks. You might want to have specific screens in Dark or Light Mode only, skip override for controls and input components. At first glance, this might seem like a bad pattern - who would like to reimplement the system which is natively working? </p><p>This strategy respects both:</p><ul><li><p>The <strong>system default</strong></p></li><li><p>The <strong>user&#8217;s explicit intent</strong></p></li></ul><p>It shifts control away from the developer and back to the user &#8212; which aligns with Apple&#8217;s Human Interface Guidelines.</p><blockquote><p>Rather than overriding because you can, you override because the user asked you to.</p></blockquote><p>Actually, global override comes &#8220;hand-to-hand&#8220; with this approach. Allowing user to pick what mode to use is handy and transparent from a UX prospective.</p><p>Here is a basic implementation with <code>AppStorage</code>:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!sEOY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26c92e1e-84ac-4bc3-9f0b-f55a89ae3f32_3452x2896.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!sEOY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26c92e1e-84ac-4bc3-9f0b-f55a89ae3f32_3452x2896.png 424w, https://substackcdn.com/image/fetch/$s_!sEOY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26c92e1e-84ac-4bc3-9f0b-f55a89ae3f32_3452x2896.png 848w, https://substackcdn.com/image/fetch/$s_!sEOY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26c92e1e-84ac-4bc3-9f0b-f55a89ae3f32_3452x2896.png 1272w, https://substackcdn.com/image/fetch/$s_!sEOY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26c92e1e-84ac-4bc3-9f0b-f55a89ae3f32_3452x2896.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!sEOY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26c92e1e-84ac-4bc3-9f0b-f55a89ae3f32_3452x2896.png" width="1456" height="1221" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/26c92e1e-84ac-4bc3-9f0b-f55a89ae3f32_3452x2896.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1221,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:6661735,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/182788128?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26c92e1e-84ac-4bc3-9f0b-f55a89ae3f32_3452x2896.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!sEOY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26c92e1e-84ac-4bc3-9f0b-f55a89ae3f32_3452x2896.png 424w, https://substackcdn.com/image/fetch/$s_!sEOY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26c92e1e-84ac-4bc3-9f0b-f55a89ae3f32_3452x2896.png 848w, https://substackcdn.com/image/fetch/$s_!sEOY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26c92e1e-84ac-4bc3-9f0b-f55a89ae3f32_3452x2896.png 1272w, https://substackcdn.com/image/fetch/$s_!sEOY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26c92e1e-84ac-4bc3-9f0b-f55a89ae3f32_3452x2896.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h3></h3>]]></content:encoded></item><item><title><![CDATA[Swift Bits: Closure Capture List]]></title><description><![CDATA[Animate State change]]></description><link>https://antongubarenko.substack.com/p/swift-bits-closure-capture-list</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swift-bits-closure-capture-list</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Tue, 23 Dec 2025 08:20:40 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/0aac7f7e-4c78-484e-bb0e-7ddad6ece7e7_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Closures are one of Swift&#8217;s most powerful features and also a common source of subtle bugs.</p><p>To understand why capture lists exist and why strong capture is the default, we need to look at how closures interact with <strong>value types</strong>, <strong>reference types</strong>, and <strong>memory ownership</strong>.</p><div><hr></div><h2>What Is a Closure?</h2><p>A closure is a self-contained block of code that can:</p><ul><li><p>Be stored in a variable</p></li><li><p>Be passed around</p></li><li><p>Capture values from its surrounding context</p></li></ul><p>In Swift,  we should care about <strong>2 main points</strong> about closures:</p><ul><li><p>They are reference type</p></li><li><p>Have capture lists to work with surrounding values/objects</p></li></ul><p>Our simple closure might look like this:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!FYDt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f287249-bab9-40c5-a84a-005c14618e70_2080x664.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!FYDt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f287249-bab9-40c5-a84a-005c14618e70_2080x664.png 424w, https://substackcdn.com/image/fetch/$s_!FYDt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f287249-bab9-40c5-a84a-005c14618e70_2080x664.png 848w, https://substackcdn.com/image/fetch/$s_!FYDt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f287249-bab9-40c5-a84a-005c14618e70_2080x664.png 1272w, https://substackcdn.com/image/fetch/$s_!FYDt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f287249-bab9-40c5-a84a-005c14618e70_2080x664.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!FYDt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f287249-bab9-40c5-a84a-005c14618e70_2080x664.png" width="500" height="159.68406593406593" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4f287249-bab9-40c5-a84a-005c14618e70_2080x664.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:465,&quot;width&quot;:1456,&quot;resizeWidth&quot;:500,&quot;bytes&quot;:933855,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/180779526?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f287249-bab9-40c5-a84a-005c14618e70_2080x664.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!FYDt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f287249-bab9-40c5-a84a-005c14618e70_2080x664.png 424w, https://substackcdn.com/image/fetch/$s_!FYDt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f287249-bab9-40c5-a84a-005c14618e70_2080x664.png 848w, https://substackcdn.com/image/fetch/$s_!FYDt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f287249-bab9-40c5-a84a-005c14618e70_2080x664.png 1272w, https://substackcdn.com/image/fetch/$s_!FYDt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f287249-bab9-40c5-a84a-005c14618e70_2080x664.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><div><hr></div><h2>What Does &#8220;Capturing&#8221; Mean?</h2><p>While reference nature of a closure is not so mystic, we can dive into second aspect. When a closure references variables defined outside its body, Swift must decide:</p><ul><li><p>Should the closure <strong>copy</strong> the value?</p></li><li><p>Or should it <strong>reference</strong> the original storage?</p></li></ul><p>The answer depends on:</p><ul><li><p>Whether the variable is a <strong>value type</strong> or <strong>reference type</strong></p></li><li><p>Whether capture is <strong>implicit</strong> or <strong>explicit</strong></p></li></ul><div><hr></div><h2>Implicit Capture (Default Behavior)</h2><p>When you don&#8217;t specify a capture list, Swift captures variables <strong>implicitly</strong>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!IUj2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe86550b2-8069-49d3-985f-312fda39fbf8_2080x1192.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!IUj2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe86550b2-8069-49d3-985f-312fda39fbf8_2080x1192.png 424w, https://substackcdn.com/image/fetch/$s_!IUj2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe86550b2-8069-49d3-985f-312fda39fbf8_2080x1192.png 848w, https://substackcdn.com/image/fetch/$s_!IUj2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe86550b2-8069-49d3-985f-312fda39fbf8_2080x1192.png 1272w, https://substackcdn.com/image/fetch/$s_!IUj2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe86550b2-8069-49d3-985f-312fda39fbf8_2080x1192.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!IUj2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe86550b2-8069-49d3-985f-312fda39fbf8_2080x1192.png" width="501" height="286.9739010989011" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e86550b2-8069-49d3-985f-312fda39fbf8_2080x1192.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:834,&quot;width&quot;:1456,&quot;resizeWidth&quot;:501,&quot;bytes&quot;:1551889,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/180779526?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe86550b2-8069-49d3-985f-312fda39fbf8_2080x1192.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!IUj2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe86550b2-8069-49d3-985f-312fda39fbf8_2080x1192.png 424w, https://substackcdn.com/image/fetch/$s_!IUj2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe86550b2-8069-49d3-985f-312fda39fbf8_2080x1192.png 848w, https://substackcdn.com/image/fetch/$s_!IUj2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe86550b2-8069-49d3-985f-312fda39fbf8_2080x1192.png 1272w, https://substackcdn.com/image/fetch/$s_!IUj2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe86550b2-8069-49d3-985f-312fda39fbf8_2080x1192.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>What&#8217;s happening?</h3><ul><li><p>value is a <strong>variable</strong>, not a constant</p></li><li><p>Swift places value into a shared heap box</p></li><li><p>Both the closure and outer scope reference the same storage</p></li></ul><p>This makes closures feel &#8220;live&#8221; and stateful.</p><div><hr></div><h2>Explicit Capture List = Snapshot</h2><p>Now add a capture list:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!iSdI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50243ed-d65e-4f6b-bfe2-925eb1a6edf3_2080x1104.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!iSdI!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50243ed-d65e-4f6b-bfe2-925eb1a6edf3_2080x1104.png 424w, https://substackcdn.com/image/fetch/$s_!iSdI!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50243ed-d65e-4f6b-bfe2-925eb1a6edf3_2080x1104.png 848w, https://substackcdn.com/image/fetch/$s_!iSdI!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50243ed-d65e-4f6b-bfe2-925eb1a6edf3_2080x1104.png 1272w, https://substackcdn.com/image/fetch/$s_!iSdI!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50243ed-d65e-4f6b-bfe2-925eb1a6edf3_2080x1104.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!iSdI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50243ed-d65e-4f6b-bfe2-925eb1a6edf3_2080x1104.png" width="500" height="265.4532967032967" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c50243ed-d65e-4f6b-bfe2-925eb1a6edf3_2080x1104.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:773,&quot;width&quot;:1456,&quot;resizeWidth&quot;:500,&quot;bytes&quot;:1451914,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/180779526?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50243ed-d65e-4f6b-bfe2-925eb1a6edf3_2080x1104.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!iSdI!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50243ed-d65e-4f6b-bfe2-925eb1a6edf3_2080x1104.png 424w, https://substackcdn.com/image/fetch/$s_!iSdI!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50243ed-d65e-4f6b-bfe2-925eb1a6edf3_2080x1104.png 848w, https://substackcdn.com/image/fetch/$s_!iSdI!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50243ed-d65e-4f6b-bfe2-925eb1a6edf3_2080x1104.png 1272w, https://substackcdn.com/image/fetch/$s_!iSdI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc50243ed-d65e-4f6b-bfe2-925eb1a6edf3_2080x1104.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Key differences:</h3><ul><li><p><code>[value]</code> captures the value <strong>at closure creation</strong></p></li><li><p>The captured value is a <strong>constant</strong></p></li><li><p>Later mutations are invisible</p></li></ul><p>This is not a memory-management feature&#8212;it&#8217;s a <strong>semantic change</strong>.</p><div><hr></div><h2>Why Explicitly Captured Values Are Immutable</h2><p>This fails to compile:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!jRBH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0a6fe80-dca3-48b0-86c6-0dd83c28bc30_2928x928.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!jRBH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0a6fe80-dca3-48b0-86c6-0dd83c28bc30_2928x928.png 424w, https://substackcdn.com/image/fetch/$s_!jRBH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0a6fe80-dca3-48b0-86c6-0dd83c28bc30_2928x928.png 848w, https://substackcdn.com/image/fetch/$s_!jRBH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0a6fe80-dca3-48b0-86c6-0dd83c28bc30_2928x928.png 1272w, https://substackcdn.com/image/fetch/$s_!jRBH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0a6fe80-dca3-48b0-86c6-0dd83c28bc30_2928x928.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!jRBH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0a6fe80-dca3-48b0-86c6-0dd83c28bc30_2928x928.png" width="601" height="190.28914835164835" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c0a6fe80-dca3-48b0-86c6-0dd83c28bc30_2928x928.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:461,&quot;width&quot;:1456,&quot;resizeWidth&quot;:601,&quot;bytes&quot;:1712093,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/180779526?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0a6fe80-dca3-48b0-86c6-0dd83c28bc30_2928x928.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!jRBH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0a6fe80-dca3-48b0-86c6-0dd83c28bc30_2928x928.png 424w, https://substackcdn.com/image/fetch/$s_!jRBH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0a6fe80-dca3-48b0-86c6-0dd83c28bc30_2928x928.png 848w, https://substackcdn.com/image/fetch/$s_!jRBH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0a6fe80-dca3-48b0-86c6-0dd83c28bc30_2928x928.png 1272w, https://substackcdn.com/image/fetch/$s_!jRBH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc0a6fe80-dca3-48b0-86c6-0dd83c28bc30_2928x928.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Because:</p><ul><li><p>Explicit capture means <em>copy</em></p></li><li><p>Copies are immutable by design</p></li><li><p>Mutating them would break determinism</p></li></ul><p>Swift forces you to be honest about intent.</p><div><hr></div><h2>The Tricky Example: Same Name, Different Reality</h2><p>Some might call it shadowing.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9ZIf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7474c6c1-6c30-4e44-b523-09b458e5986f_2080x1720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9ZIf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7474c6c1-6c30-4e44-b523-09b458e5986f_2080x1720.png 424w, https://substackcdn.com/image/fetch/$s_!9ZIf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7474c6c1-6c30-4e44-b523-09b458e5986f_2080x1720.png 848w, https://substackcdn.com/image/fetch/$s_!9ZIf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7474c6c1-6c30-4e44-b523-09b458e5986f_2080x1720.png 1272w, https://substackcdn.com/image/fetch/$s_!9ZIf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7474c6c1-6c30-4e44-b523-09b458e5986f_2080x1720.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9ZIf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7474c6c1-6c30-4e44-b523-09b458e5986f_2080x1720.png" width="500" height="413.46153846153845" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7474c6c1-6c30-4e44-b523-09b458e5986f_2080x1720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1204,&quot;width&quot;:1456,&quot;resizeWidth&quot;:500,&quot;bytes&quot;:2170073,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/180779526?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7474c6c1-6c30-4e44-b523-09b458e5986f_2080x1720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!9ZIf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7474c6c1-6c30-4e44-b523-09b458e5986f_2080x1720.png 424w, https://substackcdn.com/image/fetch/$s_!9ZIf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7474c6c1-6c30-4e44-b523-09b458e5986f_2080x1720.png 848w, https://substackcdn.com/image/fetch/$s_!9ZIf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7474c6c1-6c30-4e44-b523-09b458e5986f_2080x1720.png 1272w, https://substackcdn.com/image/fetch/$s_!9ZIf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7474c6c1-6c30-4e44-b523-09b458e5986f_2080x1720.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Each call:</p><ul><li><p>Starts from the same captured snapshot</p></li><li><p>Mutates only a local copy</p></li><li><p>Never touches outer state</p></li></ul><p>This looks stateful&#8212;but isn&#8217;t.</p><div><hr></div><h2>Reference Types</h2><p>Now let&#8217;s switch from value types to reference types.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_-Td!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2630e0ef-1e87-4f84-b827-660c286cf845_2080x1544.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_-Td!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2630e0ef-1e87-4f84-b827-660c286cf845_2080x1544.png 424w, https://substackcdn.com/image/fetch/$s_!_-Td!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2630e0ef-1e87-4f84-b827-660c286cf845_2080x1544.png 848w, https://substackcdn.com/image/fetch/$s_!_-Td!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2630e0ef-1e87-4f84-b827-660c286cf845_2080x1544.png 1272w, https://substackcdn.com/image/fetch/$s_!_-Td!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2630e0ef-1e87-4f84-b827-660c286cf845_2080x1544.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_-Td!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2630e0ef-1e87-4f84-b827-660c286cf845_2080x1544.png" width="501" height="371.9649725274725" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2630e0ef-1e87-4f84-b827-660c286cf845_2080x1544.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1081,&quot;width&quot;:1456,&quot;resizeWidth&quot;:501,&quot;bytes&quot;:1955346,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/180779526?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2630e0ef-1e87-4f84-b827-660c286cf845_2080x1544.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!_-Td!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2630e0ef-1e87-4f84-b827-660c286cf845_2080x1544.png 424w, https://substackcdn.com/image/fetch/$s_!_-Td!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2630e0ef-1e87-4f84-b827-660c286cf845_2080x1544.png 848w, https://substackcdn.com/image/fetch/$s_!_-Td!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2630e0ef-1e87-4f84-b827-660c286cf845_2080x1544.png 1272w, https://substackcdn.com/image/fetch/$s_!_-Td!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2630e0ef-1e87-4f84-b827-660c286cf845_2080x1544.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Here:</p><ul><li><p>The closure captures a <strong>reference</strong></p></li><li><p>Both scopes point to the same instance</p></li><li><p>Mutations are shared</p></li></ul><p>Even if you write:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!GwXJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12a12dc1-2375-4309-ab89-2b51261bb1db_2080x664.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!GwXJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12a12dc1-2375-4309-ab89-2b51261bb1db_2080x664.png 424w, https://substackcdn.com/image/fetch/$s_!GwXJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12a12dc1-2375-4309-ab89-2b51261bb1db_2080x664.png 848w, https://substackcdn.com/image/fetch/$s_!GwXJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12a12dc1-2375-4309-ab89-2b51261bb1db_2080x664.png 1272w, https://substackcdn.com/image/fetch/$s_!GwXJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12a12dc1-2375-4309-ab89-2b51261bb1db_2080x664.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!GwXJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12a12dc1-2375-4309-ab89-2b51261bb1db_2080x664.png" width="500" height="159.68406593406593" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/12a12dc1-2375-4309-ab89-2b51261bb1db_2080x664.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:465,&quot;width&quot;:1456,&quot;resizeWidth&quot;:500,&quot;bytes&quot;:938867,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/180779526?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12a12dc1-2375-4309-ab89-2b51261bb1db_2080x664.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!GwXJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12a12dc1-2375-4309-ab89-2b51261bb1db_2080x664.png 424w, https://substackcdn.com/image/fetch/$s_!GwXJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12a12dc1-2375-4309-ab89-2b51261bb1db_2080x664.png 848w, https://substackcdn.com/image/fetch/$s_!GwXJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12a12dc1-2375-4309-ab89-2b51261bb1db_2080x664.png 1272w, https://substackcdn.com/image/fetch/$s_!GwXJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12a12dc1-2375-4309-ab89-2b51261bb1db_2080x664.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>You are still mutating shared state, because the reference itself was copied, not the object.</p><div><hr></div><h2>Why strong Capture Is the Default</h2><p>Swift closures capture <strong>strongly by default</strong>, especially for reference types for different reasons. Some of them are architectural, others are because of types implementation.</p><h3>1. Predictability</h3><p>If closures captured weakly by default, objects could disappear unexpectedly:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zjwP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ec90e90-0656-498c-8c40-5defc41e40ee_2080x664.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zjwP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ec90e90-0656-498c-8c40-5defc41e40ee_2080x664.png 424w, https://substackcdn.com/image/fetch/$s_!zjwP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ec90e90-0656-498c-8c40-5defc41e40ee_2080x664.png 848w, https://substackcdn.com/image/fetch/$s_!zjwP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ec90e90-0656-498c-8c40-5defc41e40ee_2080x664.png 1272w, https://substackcdn.com/image/fetch/$s_!zjwP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ec90e90-0656-498c-8c40-5defc41e40ee_2080x664.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zjwP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ec90e90-0656-498c-8c40-5defc41e40ee_2080x664.png" width="500" height="159.68406593406593" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1ec90e90-0656-498c-8c40-5defc41e40ee_2080x664.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:465,&quot;width&quot;:1456,&quot;resizeWidth&quot;:500,&quot;bytes&quot;:948798,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/180779526?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ec90e90-0656-498c-8c40-5defc41e40ee_2080x664.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!zjwP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ec90e90-0656-498c-8c40-5defc41e40ee_2080x664.png 424w, https://substackcdn.com/image/fetch/$s_!zjwP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ec90e90-0656-498c-8c40-5defc41e40ee_2080x664.png 848w, https://substackcdn.com/image/fetch/$s_!zjwP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ec90e90-0656-498c-8c40-5defc41e40ee_2080x664.png 1272w, https://substackcdn.com/image/fetch/$s_!zjwP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ec90e90-0656-498c-8c40-5defc41e40ee_2080x664.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Strong capture guarantees:</p><blockquote><p>&#8220;If this closure runs, everything it needs still exists.&#8221;</p></blockquote><h3>2. Safety Over Convenience</h3><p>Unexpected deallocation is far worse than a retain cycle.</p><p>Swift chooses:</p><ul><li><p><strong>Explicit memory leaks</strong> over</p></li><li><p><strong>Implicit crashes</strong></p></li></ul><p>This is why <code>[weak self]</code> is opt-in.</p><h3>3. Closures Are Ownership Boundaries</h3><p>A closure is a unit of work.</p><p>If it references an object, it <strong>owns</strong> it unless told otherwise.</p><p>That aligns with Swift&#8217;s philosophy:</p><blockquote><p>Ownership must be explicit when it changes.</p></blockquote><div><hr></div><h2>Capture Lists Are About Semantics, Not Just Memory</h2><p>Many developers think capture lists exist only to avoid retain cycles. That&#8217;s incomplete.</p><p>Capture lists control:</p><ul><li><p><strong>When</strong> values are captured</p></li><li><p><strong>Whether</strong> state is shared</p></li><li><p><strong>How</strong> mutations behave</p></li><li><p><strong>What</strong> the closure truly depends on</p></li></ul><p>Memory management is just one side effect.</p><div><hr></div><h2>Final Takeaway</h2><p>Closures don&#8217;t just run code. They <strong>share</strong>, or <strong>own</strong> parts of your program.</p><p>Understanding capture lists means understanding:</p><ul><li><p>Value vs reference semantics</p></li><li><p>Ownership</p></li><li><p>Time (when state is captured)</p></li></ul><p>And once you see that, Swift&#8217;s rules stop being surprising&#8212;and start being precise.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Swift Bits: Autoreleasepool Usage]]></title><description><![CDATA[MCR survivor and helper]]></description><link>https://antongubarenko.substack.com/p/swift-bits-autoreleasepool-usage</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swift-bits-autoreleasepool-usage</guid><pubDate>Tue, 16 Dec 2025 08:20:15 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/9bce5718-de7a-440f-b35a-a3cd652601b7_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>iOS development might not have the longest history compared to other platforms, but it has still gone through several epochs.From MRC (Manual Reference Counting) to ARC (Automatic Reference Counting), Objective-C to Swift transition (which is not even totally done inside Apple). One of the concepts (or mechanisms, I should say) is <code>NSAutoreleasePool</code> or just <code>autoreleasepool</code> (for Swift). </p><p>And yes, that question might come up in a tech interview as well.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h2>What is autoreleasepool?</h2><p>An autorelease pool is a container that temporarily holds objects sent an autorelease message.</p><p>When the pool is drained, all objects inside it receive a release. This takes us back to when we had to pair <code>retain/release</code> calls to manage memory and to reduce the lifetime (<code>release </code>call) for an object until a specific scope is finished. </p><p>Syntax is pretty simple:</p><pre><code>autoreleasepool {
    // Autoreleased objects
}</code></pre><p>Even under ARC, autorelease pools still exist because:</p><ul><li><p>Many system APIs are written in Objective-C</p></li><li><p>Bridging between Swift and Objective-C often produces autoreleased objects</p></li><li><p>Some frameworks intentionally return autoreleased values for performance</p></li></ul><div><hr></div><h2>When is autoreleasepool used by default?</h2><p>You usually don&#8217;t see it &#8212; but it&#8217;s there.</p><h3>Run Loop Boundaries (Main Thread)</h3><p>According to the <a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmAutoreleasePools.html">documentation</a>, every iteration of the main run loop creates an autorelease pool automatically.</p><pre><code>Run loop iteration
 &#9500;&#9472; create autorelease pool
 &#9500;&#9472; handle events
 &#9500;&#9472; drain autorelease pool</code></pre><p>This means:</p><ul><li><p>UI events</p></li><li><p>Touch handling</p></li><li><p>Timers</p></li><li><p>Display updates</p></li></ul><p>&#8230;all benefit from automatic memory cleanup at the end of each loop.</p><h3>App Entry Point (main)</h3><p>UIKit and Swift runtime wrap your app&#8217;s execution in an autorelease pool.</p><p>Objective-C (conceptually):</p><pre><code>int main() {
    @autoreleasepool {
        UIApplicationMain(...)
    }
}</code></pre><p>Swift does the same internally.</p><h3>Objective-C APIs Returning Autoreleased Objects</h3><p>For example:</p><pre><code>NSString *s = [NSString stringWithFormat:@&#8221;%@&#8221;, value];
NSArray *a = [NSArray arrayWithObjects:...];</code></pre><p>Even when used from Swift, these APIs may generate autoreleased objects under the hood.</p><div><hr></div><h2>Where can (and should) you use autoreleasepool manually?</h2><p>This is the main question when it comes to calling our old friend. Cases are pretty rare, since ARC does a lot for us now. And still for peak performance optimization - useful to know that we still have this tool.</p><p>Autorelease pools are especially helpful when you need finer control over memory usage. They shine in resource-heavy scenarios such as processing large data sets, parsing XML or JSON, and repeatedly loading or releasing views.</p><h3>Tight Loops Creating Many Objects</h3><p>This is the <strong>most important use case</strong>.</p><pre><code>for i in 0..&lt;10_000 {
    autoreleasepool {
        let image = UIImage(contentsOfFile: path)
        process(image)
    }
}</code></pre><p>Without a local pool:</p><ul><li><p>Objects live until the <strong>outer run loop pool drains</strong></p></li><li><p>Memory spikes dramatically</p></li></ul><p>With a pool:</p><ul><li><p>Memory is released <strong>per iteration</strong></p></li></ul><h3>Background Threads &amp; GCD Queues</h3><p>Unlike the main thread, <strong>background queues do not automatically create autorelease pools</strong>.</p><pre><code>DispatchQueue.global().async {
    autoreleasepool {
        // Objective-C objects
    }
}</code></pre><p>This is critical when:</p><ul><li><p>Processing images</p></li><li><p>Parsing large files</p></li><li><p>Using Core Graphics, Core Image, or Foundation APIs</p></li></ul><h3>Command-Line Tools &amp; Scripts</h3><p>I&#8217;ve warned you about rare cases. In Swift CLI tools:</p><pre><code>autoreleasepool {
    runTool()
}</code></pre><p>Without it:</p><ul><li><p>Autoreleased objects may never be drained until process exit</p></li><li><p>Memory usage grows unnecessarily</p></li></ul><h3>Interacting with Core Foundation / C APIs</h3><p>Did I mention rare cases? Some APIs create bridged Objective-C objects that rely on autorelease pools:</p><ul><li><p>Core Image</p></li><li><p>AVFoundation</p></li><li><p>PDFKit</p></li><li><p>Metal tools that bridge to Foundation</p></li></ul><div><hr></div><h2>Summary</h2><ul><li><p><code>autoreleasepool</code> is <strong>not obsolete</strong>, even under ARC</p></li><li><p>It controls when temporary Objective-C objects are released</p></li><li><p>Automatically created:</p><ul><li><p>Per main run loop iteration</p></li><li><p>Around app entry points</p></li></ul></li><li><p>You should create one manually:</p><ul><li><p>In tight loops</p></li><li><p>On background threads</p></li><li><p>In CLI tools</p></li></ul></li><li><p>Swift developers still need it when working with Objective-C frameworks</p></li></ul><div><hr></div><h2>References</h2><ul><li><p><a href="https://developer.apple.com/documentation/foundation/nsautoreleasepool">NSAutoreleasePool</a></p></li><li><p><a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmAutoreleasePools.html">Using Autorelease Pool Blocks</a></p></li><li><p><a href="https://forums.swift.org/t/the-role-of-autoreleasepool-in-swift-and-thoughts-about-memory-management-in-swift/52976">The role of autoreleasepool in Swift and thoughts about memory management in Swift</a></p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Swift Bits: @dynamic vs @objc]]></title><description><![CDATA[Animate State change]]></description><link>https://antongubarenko.substack.com/p/swift-bits-dynamic-vs-objc</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swift-bits-dynamic-vs-objc</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Tue, 09 Dec 2025 08:39:12 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/3ad98a43-e287-4432-99ee-be4a35ce1eb6_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You have probably, at some point, become aware of dispatch methods:</p><ul><li><p>Static</p></li><li><p>Dynamic</p></li><li><p>Message dispatch</p></li></ul><p>Explaining each of them has been done many times and continues to bring new insights. Here are some valuable links to help you get the full picture:</p><ul><li><p><a href="https://medium.com/@pallavidipke07/method-dispatch-in-swift-b113a40a713a">Method Dispatch in Swift</a> </p></li><li><p><a href="https://blog.jacobstechtavern.com/p/compiler-cocaine-the-swift-method">The Swift Method Dispatch Deep Dive</a></p></li><li><p><a href="https://fritz.ai/method-dispatch-in-swift/">Understanding method dispatch in Swift</a></p></li></ul><h3>What&#8217;s the Difference?</h3><p>Both of them come from the Objective-C world when we had only Message Dispatch using <code>objc_msgSend</code>.</p><blockquote><p><code>@objc</code> <strong>exposes things to the Objective-C runtime</strong>, but by itself <strong>does not always force Objective-C message dispatch for Swift callers</strong>.</p></blockquote><p>Swift adds <code>@objc</code> implicitly when:</p><ul><li><p>You subclass <code>NSObject</code></p></li><li><p>You override an Objective-C method</p></li><li><p>You conform to an <code>@objc</code> protocol </p></li></ul><p>In other cases, behavior varies depending on where it&#8217;s added.</p><div><hr></div><p>For a property:</p><pre><code>class Person: NSObject {
    @objc var name: String = &#8220;&#8221;
}</code></pre><p>What this does:</p><ol><li><p>The <strong>getter</strong> and <strong>setter</strong> are exposed as Objective-C methods:</p><ul><li><p>- (NSString *)name</p></li><li><p>- (void)setName:(NSString *)name</p></li></ul></li><li><p>The property becomes visible to:</p><ul><li><p>Objective-C code (normal property access)</p></li><li><p>KVC (<code>value(forKey: &#8220;name&#8221;)</code>, <code>setValue(_:forKey:)</code>)</p></li></ul></li></ol><div><hr></div><p>For a class:</p><pre><code>@objc class MyController: NSObject {
    @objc func doStuff() { }
}</code></pre><p>What this does:</p><ol><li><p>Registers the class with the Objective-C runtime (as long as it&#8217;s Obj-C compatible, typically via <code>NSObject</code>).</p></li><li><p>The class is now visible in Objective-C:</p><ul><li><p><code>MyController *c = [[MyController alloc] init];</code></p></li><li><p>Can be used in Objective-C APIs, storyboards, etc.</p></li></ul></li></ol><p>Important points:</p><ul><li><p>Marking a <strong>class</strong> with <code>@obj</code>c does <strong>not</strong> automatically make all members <code>@objc</code>. You must either:</p><ul><li><p>Mark members individually with <code>@objc</code>, or</p></li><li><p>Use <code>@objcMembers</code> on the class:</p><pre><code>@objcMembers
class MyController: NSObject {
    func foo() { }  // implicitly @objc
}</code></pre></li></ul></li></ul><div><hr></div><blockquote><p><code>dynamic</code> (usually used as <code>@objc dynamic</code>) is what <strong>forces</strong> Objective-C&#8211;style message dispatch even from Swift.</p></blockquote><p>In Swift, this forces dynamic dispatch. And that&#8217;s something confusing for Objective-C old-timers.</p><div><hr></div><h3>Tricky Story</h3><p>If you remember @<code>dynamic</code> from Objective-C, it will NOT generate getter/setter and it says: &#8220;You will find out the implementation later, at runtime&#8220;.</p><p>In Swift, @NSManaged is used to postpone getter/setter generation:</p><pre><code>class Person: NSManagedObject {
    @NSManaged var name: String
}</code></pre><div><hr></div><h3>Summary</h3><ul><li><p><code>@objc</code>:</p><ul><li><p>Makes the symbol participate in the Objective-C runtime (gives it a selector, etc.).</p></li><li><p>Ensures Objective-C callers use message dispatch.</p></li><li><p><strong>Does not forbid</strong> the Swift compiler from using static/vtable dispatch for Swift call sites.</p></li></ul></li><li><p><code>dynamic</code> (which implies <code>@objc</code>):</p><ul><li><p><strong>Does force</strong> the use of Objective-C dynamic dispatch (Message Dispatch) (via <code>objc_msgSend</code>) for <em>all</em> calls, including from Swift.</p></li><li><p>This is what you need for KVO, method swizzling, and other runtime features.</p></li></ul></li></ul><p>If you want a simple rule of thumb:</p><ul><li><p><strong>Need Obj-C interop only?</strong> &#8594; @objc</p></li><li><p><strong>Need KVO / swizzling?</strong> &#8594; @objc dynamic<a href="https://fritz.ai/method-dispatch-in-swift/"><br></a></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p><a href="https://fritz.ai/method-dispatch-in-swift/"><br></a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[SwiftUI: Charts Interactivity - Part 2]]></title><description><![CDATA[Add selection for data representation]]></description><link>https://antongubarenko.substack.com/p/swiftui-charts-interactivity-part-1ed</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swiftui-charts-interactivity-part-1ed</guid><pubDate>Mon, 08 Dec 2025 08:46:34 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/c51898a9-9b32-46a1-b909-b91c5c68b231_592x682.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the previous <a href="https://antongubarenko.substack.com/p/swiftui-charts-interactivity-part?r=21t43r">part</a>, we built a humidity chart using the native Swift Charts framework. It has a nice X-axis with visible dates and a Y-axis with dynamic calculations and marks for noticeable humidity spikes.</p><p>Let me show you once again:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!DrWr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F23d580e8-d705-462e-8195-909b0ac5ed1b_592x632.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!DrWr!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F23d580e8-d705-462e-8195-909b0ac5ed1b_592x632.png 424w, https://substackcdn.com/image/fetch/$s_!DrWr!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F23d580e8-d705-462e-8195-909b0ac5ed1b_592x632.png 848w, https://substackcdn.com/image/fetch/$s_!DrWr!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F23d580e8-d705-462e-8195-909b0ac5ed1b_592x632.png 1272w, https://substackcdn.com/image/fetch/$s_!DrWr!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F23d580e8-d705-462e-8195-909b0ac5ed1b_592x632.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!DrWr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F23d580e8-d705-462e-8195-909b0ac5ed1b_592x632.png" width="380" height="405.6756756756757" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/23d580e8-d705-462e-8195-909b0ac5ed1b_592x632.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:632,&quot;width&quot;:592,&quot;resizeWidth&quot;:380,&quot;bytes&quot;:118307,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/180046860?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F23d580e8-d705-462e-8195-909b0ac5ed1b_592x632.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!DrWr!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F23d580e8-d705-462e-8195-909b0ac5ed1b_592x632.png 424w, https://substackcdn.com/image/fetch/$s_!DrWr!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F23d580e8-d705-462e-8195-909b0ac5ed1b_592x632.png 848w, https://substackcdn.com/image/fetch/$s_!DrWr!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F23d580e8-d705-462e-8195-909b0ac5ed1b_592x632.png 1272w, https://substackcdn.com/image/fetch/$s_!DrWr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F23d580e8-d705-462e-8195-909b0ac5ed1b_592x632.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Chart with visible dates and humidity</figcaption></figure></div><p>The chart also supports basic selection with date highlight:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Dq72!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde2de077-a85c-450e-bcb0-bd69ea0ec322_592x704.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Dq72!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde2de077-a85c-450e-bcb0-bd69ea0ec322_592x704.png 424w, https://substackcdn.com/image/fetch/$s_!Dq72!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde2de077-a85c-450e-bcb0-bd69ea0ec322_592x704.png 848w, https://substackcdn.com/image/fetch/$s_!Dq72!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde2de077-a85c-450e-bcb0-bd69ea0ec322_592x704.png 1272w, https://substackcdn.com/image/fetch/$s_!Dq72!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde2de077-a85c-450e-bcb0-bd69ea0ec322_592x704.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Dq72!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde2de077-a85c-450e-bcb0-bd69ea0ec322_592x704.png" width="382" height="454.27027027027026" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/de2de077-a85c-450e-bcb0-bd69ea0ec322_592x704.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:704,&quot;width&quot;:592,&quot;resizeWidth&quot;:382,&quot;bytes&quot;:123539,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/180046860?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde2de077-a85c-450e-bcb0-bd69ea0ec322_592x704.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Dq72!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde2de077-a85c-450e-bcb0-bd69ea0ec322_592x704.png 424w, https://substackcdn.com/image/fetch/$s_!Dq72!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde2de077-a85c-450e-bcb0-bd69ea0ec322_592x704.png 848w, https://substackcdn.com/image/fetch/$s_!Dq72!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde2de077-a85c-450e-bcb0-bd69ea0ec322_592x704.png 1272w, https://substackcdn.com/image/fetch/$s_!Dq72!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fde2de077-a85c-450e-bcb0-bd69ea0ec322_592x704.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Selection with date info</figcaption></figure></div><p>It&#8217;s time to improve it!</p><div><hr></div><h3>Custom Selection Handling</h3><p>Our selection relies on:</p><pre><code>.chartXSelection(value: $selectedX)</code></pre><p>With it we are binding to values on X-axis: dates in our case. However, our tweak will require an old implementation using <a href="https://developer.apple.com/documentation/swiftui/view/chartoverlay(alignment:content:)/">chartOverlay</a> which returns <a href="https://developer.apple.com/documentation/Charts/ChartProxy">ChartProxy</a>. It helps convert the tap point into an actual value on the axis.</p><pre><code><code>.chartOverlay { proxy in
    GeometryReader { geometry in
        Rectangle()
            .fill(.clear)
            .contentShape(Rectangle())
            .gesture(
                DragGesture()
                    .onChanged { value in
                        // Convert the gesture location to the plot area&#8217;s coordinate space.
                        let origin = geometry[proxy.plotAreaFrame].origin
                        guard let plotFrame = proxy.plotFrame else {return}
                                let origin = geometry[plotFrame].origin
                                let location = CGPoint(
                                    x: value.location.x - origin.x,
                                    y: value.location.y - origin.y
                                )
                                // Get the x (date) and y (humidity) value from the location.
                                let (date, humidity) = proxy.value(at: location, as: (Date, Int).self) ?? (Date(), 0)
                        
                        // Clamp to real data range.
                        if
                            let firstDate = data.first?.date,
                            let lastDate = data.last?.date,
                            date &gt;= firstDate,
                            date &lt;= lastDate
                        {
                            selectedX = date
                        }
                    }
                    .onEnded { _ in
                        selectedX = nil
                    }
            )
    }
}</code></code></pre><p>In this modifier, we:</p><ul><li><p>Using the GeometryReader with Rectangle which takes the whole width and height of a View</p></li><li><p>Assigning a DragGesture to get a tap location</p></li><li><p>Adjusting location based on chart origin</p></li><li><p>Converting adjusted tap location to values from data set</p></li><li><p>Limiting the <code>selectedX</code> with only actual dates</p></li></ul><p>This codes makes the same as <code>chartXSelection</code> with fill control of the range.</p><div><hr></div><h3>Jump to Closest Values</h3><p>During selection we can drag to mid values between grid steps like on picture above. That&#8217;s not very informative sometimes and selecting only actual steps will be a good option.</p><p>First, need to calculate the closest date to selected one. A small extension will help with that:</p><pre><code>extension Date {
    func closestDate(in dates: [Date]) -&gt; Self? {
        guard !dates.isEmpty else { return nil }
        return dates.min(by: { abs($0.timeIntervalSince(self)) &lt; abs($1.timeIntervalSince(self)) })
    }
}</code></pre><p>Then, on handling the drag:</p><pre><code>.chartOverlay { proxy in
    GeometryReader { geometry in
        Rectangle()
            .fill(.clear)
            .contentShape(Rectangle())
            .gesture(
                DragGesture()
                    .onChanged { value in
                        guard let plotFrame = proxy.plotFrame else { return }
                        
                        // Convert the gesture location to the plot area&#8217;s coordinate space.
                        let origin = geometry[plotFrame].origin
                        let location = CGPoint(
                            x: value.location.x - origin.x,
                            y: value.location.y - origin.y
                        )
                        
                        // Get the x (date) and y (humidity) value from the location.
                        let (date, humidity) = proxy.value(
                            at: location,
                            as: (Date, Int).self
                        ) ?? (Date(), 0)
                        
                        // Clamp to valid data range.
                        if
                            let firstDate = data.first?.date,
                            let lastDate = data.last?.date,
                            date &gt;= firstDate,
                            date &lt;= lastDate
                        {
                            // Calculate closest date.
                            let dates = data.map(\.date)
                            let closestDate = date.closestDate(in: dates)
                            selectedX = closestDate
                        }
                    }
                    .onEnded { _ in
                        selectedX = nil
                    }
            )
    }
}</code></pre><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;5aa3b5cc-7983-48ee-b333-4ef33453424a&quot;,&quot;duration&quot;:null}"></div><div><hr></div><h3>X-axis Interpolation</h3><p>Years ago, I had a task not just to show a vertical line on current selection, but also highlight the current X-value on line curve. With limited data set we either have to:</p><ul><li><p>Add extra values to make steps smaller</p></li><li><p>Calculate the values using the same formula that plots the chart line</p></li></ul><p>Adding values is a controversial step. And breaks the Source-of-Truth paradigm. The best solution is to go with pure math!</p><p>We have <code>.catmullRom</code> interpolation, which is well described on <a href="https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline">Wikipedia</a>.</p><p>Basically (or not) to plot 2 points spline we need:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!w1Fs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e6f7e88-c8c2-47b7-91f7-5b2d236ed149_800x600.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!w1Fs!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e6f7e88-c8c2-47b7-91f7-5b2d236ed149_800x600.png 424w, https://substackcdn.com/image/fetch/$s_!w1Fs!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e6f7e88-c8c2-47b7-91f7-5b2d236ed149_800x600.png 848w, https://substackcdn.com/image/fetch/$s_!w1Fs!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e6f7e88-c8c2-47b7-91f7-5b2d236ed149_800x600.png 1272w, https://substackcdn.com/image/fetch/$s_!w1Fs!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e6f7e88-c8c2-47b7-91f7-5b2d236ed149_800x600.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!w1Fs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e6f7e88-c8c2-47b7-91f7-5b2d236ed149_800x600.png" width="392" height="294" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6e6f7e88-c8c2-47b7-91f7-5b2d236ed149_800x600.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:600,&quot;width&quot;:800,&quot;resizeWidth&quot;:392,&quot;bytes&quot;:25160,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/180046860?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e6f7e88-c8c2-47b7-91f7-5b2d236ed149_800x600.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!w1Fs!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e6f7e88-c8c2-47b7-91f7-5b2d236ed149_800x600.png 424w, https://substackcdn.com/image/fetch/$s_!w1Fs!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e6f7e88-c8c2-47b7-91f7-5b2d236ed149_800x600.png 848w, https://substackcdn.com/image/fetch/$s_!w1Fs!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e6f7e88-c8c2-47b7-91f7-5b2d236ed149_800x600.png 1272w, https://substackcdn.com/image/fetch/$s_!w1Fs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e6f7e88-c8c2-47b7-91f7-5b2d236ed149_800x600.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Courtesy of Wikipedia</figcaption></figure></div><ul><li><p>4 points</p></li><li><p>1 normalized parameter <code>t</code></p></li></ul><p>Direct function in Swift:</p><pre><code>func catmullRom(_ p0: Double, _ p1: Double, _ p2: Double, _ p3: Double, t: Double) -&gt; Double {
        let t2 = t * t
        let t3 = t2 * t
        
        return 0.5 * (
            (2 * p1) +
            (-p0 + p2) * t +
            (2*p0 - 5*p1 + 4*p2 - p3) * t2 +
            (-p0 + 3*p1 - 3*p2 + p3) * t3
        )
    }</code></pre><p>Now we just need to pass parameters to it. And the code is a little bit bigger:</p><pre><code>// Returns an interpolated humidity value at a specific date using Catmull&#8211;Rom spline interpolation.
    /// - Parameters:
    ///   - date: The target date for interpolation.
    ///   - data: Array of humidity points sorted by date.
    /// - Returns: Interpolated humidity as `Double`, or `nil` if interpolation cannot be performed.
    func interpolatedHumidity(at date: Date, data: [HumidityRate]) -&gt; Double? {
        // Need at least 4 control points for Catmull&#8211;Rom interpolation.
        guard data.count &gt;= 4 else { return nil }
        
        // Find the first index where the data point&#8217;s date is &gt;= target date.
        // This identifies the segment the target date falls into.
        guard let idx = data.firstIndex(where: { $0.date &gt;= date }), idx &gt; 0 else {
            return nil
        }

        // Select four surrounding control points (with safe bounds)
        let i0 = max(0, idx - 2)
        let i1 = max(0, idx - 1)
        let i2 = idx
        let i3 = min(data.count - 1, idx + 1)

        // Extract humidity values (converted to Double for interpolation)
        let p0 = Double(data[i0].humidity)
        let p1 = Double(data[i1].humidity)
        let p2 = Double(data[i2].humidity)
        let p3 = Double(data[i3].humidity)

        // Compute normalized parameter t in [0, 1] along the i1&#8211;i2 segment
        let x1 = data[i1].date.timeIntervalSince1970
        let x2 = data[i2].date.timeIntervalSince1970
        let t  = (date.timeIntervalSince1970 - x1) / (x2 - x1)

        // Final Catmull&#8211;Rom interpolation
        return catmullRom(p0, p1, p2, p3, t: t)
    }</code></pre><p>To use this beautiful piece of code small changes required on values calculation on drag:</p><pre><code>// Set the selected date.
selectedX = date

// Calculate interpolated humidity (Y-value) for the selected date.
if let humidity = interpolatedHumidity(at: date, data: data) {
    selectedY = Int(humidity)
}</code></pre><p>And what&#8217;s left:</p><ul><li><p>Add <code>var selectedY</code> to track selection changes</p></li><li><p>Add new point mark when both <code>selectedX</code> and <code>selectedY</code> exist</p></li></ul><pre><code>struct HumidityChartViewDemo: View {
    
    @State private var selectedX: Date? = nil
    @State private var selectedY: Int? = nil
    ...

    if let selectedX {
                RuleMark(x: .value(&#8221;Selected&#8221;, selectedX))
                    .annotation(position: .top) {
                        VStack(spacing: 2) {
                            Text(selectedX, style: .time)
                        }
                    }
                if let selectedY {
                    PointMark(
                        x: .value(&#8221;X&#8221;, selectedX),
                        y: .value(&#8221;Y&#8221;, selectedY)
                    )
                }
                
            }
}</code></pre><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;e7fc003f-59ff-4a51-96ac-a17fd576cadc&quot;,&quot;duration&quot;:null}"></div><p>Amazing! Life could be a dream with just a few lines of code.</p><p><a href="https://gist.github.com/lanserxt/1697dcb243bbc436333f6550f8eaa101">Code for this part</a></p><p>Thanks for reading and stay tuned for the next discoveries!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Swift Bits: SwiftUI - Animate Binding]]></title><description><![CDATA[My personal Substack (and that's true!) about iOS Development]]></description><link>https://antongubarenko.substack.com/p/swift-bits-swiftui-animate-binding</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swift-bits-swiftui-animate-binding</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Wed, 03 Dec 2025 13:04:47 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/e510f22e-ccc7-44b8-8b6d-7067c561e6a6_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Small Intro</h2><p>After months of publishing articles, you eventually reach a barrier. On one side, there&#8217;s an interesting topic or challenge &#8212; and on the other, there&#8217;s the question of how much information you can disclose to make it a full post. Substack, after all, is geared toward longer pieces with animations, code snippets, and videos.</p><p>Post after post, you build trust with your audience and set an expectation level. No, I don&#8217;t mean suddenly switching to baking or gardening &#128516;. With limited tag options (and they&#8217;re barely used here), the only thing left is the <strong>title</strong> and <strong>subtitle</strong>. So why not create a category and set the expected content type in advance?</p><p>In these <strong>Swift Bits</strong> posts, I want to share small but useful tips discovered during development or while preparing articles. If you&#8217;re a beatboxer or any kind of musician in your non-coding hours (hmm, spending free time <em>not</em> coding? What nonsense!) &#8212; you&#8217;ll recognize &#8220;Bits&#8221; as those small sound pieces that make a whole track shine.</p><div><hr></div><p>To make transitions and modifier property changes, we use the .animation modifier. However, in some situations, it&#8217;s hard to trigger since we can&#8217;t just write:</p><pre><code>withAnimation {
   variable.toggle()
}</code></pre><p>Let&#8217;s check a sample:</p><pre><code>struct SwiftBits: View {
    
    //Some mode
    enum PickerMode {
        case left, right
    }
    
    @State private var mode: PickerMode = .left
    
    var body: some View {
        
        VStack {
            
            //Button to toggle the mode
            Button {
                withAnimation {
                    mode = (mode != .left) ? .left : .right
                }
            } label: {
                Text(&#8221;Change mode&#8221;)
                    .font(.title)
            }
            
            if mode == .left {
                Text(&#8221;Left&#8221;)
                    .transition(.opacity.combined(with:.scale))
            } else {
                Text(&#8221;Right&#8221;)
                    .transition(.opacity.combined(with:.scale))
            }
        }
    }
}</code></pre><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;a828b936-2fd0-4f07-9a96-0ca083013a21&quot;,&quot;duration&quot;:null}"></div><p>Everything looks good. We have an appearance transition combining opacity and scale.</p><p>But what if we want to toggle it using a <strong>Picker</strong>?</p><div><hr></div><pre><code>struct SwiftBits: View {
    
    //Some mode
    enum PickerMode {
        case left, right
    }
    
    @State private var mode: PickerMode = .left
    
    var body: some View {
        
        VStack {
            //Button to toggle the mode
            Button {
                withAnimation {
                    mode = (mode != .left) ? .left : .right
                }
            } label: {
                Text(&#8221;Change mode&#8221;)
                    .font(.title)
            }

            //Picker to toggle the mode
            Picker(&#8221;Picker&#8221;, selection: $mode) {
                Text(&#8221;Left&#8221;).tag(PickerMode.left)
                Text(&#8221;Right&#8221;).tag(PickerMode.right)
            }
            .pickerStyle(SegmentedPickerStyle())
            .padding()
            
            if mode == .left {
                Text(&#8221;Left&#8221;)
                    .transition(.opacity.combined(with:.scale))
            } else {
                Text(&#8221;Right&#8221;)
                    .transition(.opacity.combined(with:.scale))
            }
        }
    }
}</code></pre><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;78135883-757a-4c7d-9f08-417cbebfd7e2&quot;,&quot;duration&quot;:null}"></div><p>Unfortunately, this doesn&#8217;t work as expected. One option, is to write a custom Binding with get/set:</p><pre><code>var body: some View {
        let animatedModeBinding = Binding&lt;PickerMode&gt;(
                    get: { mode },
                    set: { newValue in
                        withAnimation {
                            mode = newValue
                        }
                    }
                )</code></pre><p>This looks like too much for such simple logic, and there&#8217;s a more convenient solution!</p><p><code>Binding</code> has a lot of extensions, and one of them will help us:</p><pre><code>@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension Binding {

    /// Specifies a transaction for the binding.
    ///
    /// - Parameter transaction  : An instance of a ``Transaction``.
    ///
    /// - Returns: A new binding.
    public func transaction(_ transaction: Transaction) -&gt; Binding&lt;Value&gt;

    /// Specifies an animation to perform when the binding value changes.
    ///
    /// - Parameter animation: An animation sequence performed when the binding
    ///   value changes.
    ///
    /// - Returns: A new binding.
    public func animation(_ animation: Animation? = .default) -&gt; Binding&lt;Value&gt;
}</code></pre><p><code>.animation</code> will return new Binding with animation attached.</p><div><hr></div><pre><code>struct SwiftBits: View {
    
    //Some mode
    enum PickerMode {
        case left, right
    }
    
    @State private var mode: PickerMode = .left
    
    var body: some View {

        VStack {
            //Button to toggle the mode
            Button {
                withAnimation {
                    mode = (mode != .left) ? .left : .right
                }
            } label: {
                Text(&#8221;Change mode&#8221;)
                    .font(.title)
            }

            //Picker to toggle the mode
            Picker(&#8221;Picker&#8221;, selection: $mode.animation()) {
                Text(&#8221;Left&#8221;).tag(PickerMode.left)
                Text(&#8221;Right&#8221;).tag(PickerMode.right)
            }
            .pickerStyle(SegmentedPickerStyle())
            .padding()
            
            if mode == .left {
                Text(&#8221;Left&#8221;)
                    .transition(.opacity.combined(with:.scale))
            } else {
                Text(&#8221;Right&#8221;)
                    .transition(.opacity.combined(with:.scale))
            }
        }
    }
}</code></pre><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;97f843cc-15c2-4e2b-974a-a6d83b3b3b2b&quot;,&quot;duration&quot;:null}"></div><p>Working like a charm! <a href="https://gist.github.com/lanserxt/c4eecabfb8f7d9272a8b3d88b61cabc1">Gist is here</a>.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[SwiftUI: Charts Interactivity - Part 1]]></title><description><![CDATA[Add selection for data representation]]></description><link>https://antongubarenko.substack.com/p/swiftui-charts-interactivity-part</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swiftui-charts-interactivity-part</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Mon, 01 Dec 2025 09:28:18 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/fa0d319c-ef8b-4c04-840d-58d7c3523427_1024x1024.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the previous <a href="https://antongubarenko.substack.com/p/swiftui-discardable-slider">post</a>, we&#8217;ve made a discardable slider to prevent extra data writes and logic triggering. Project is growing and now we need to represent the stored data in an understandable and appealing way - charts (if it can be applied to charts at all). Now it&#8217;s become more convenient with <a href="https://developer.apple.com/documentation/charts">Swift Charts Framework</a>.</p><p>Before iOS 16, we had to use custom charts or implement them on our own. Some of the popular solutions were::</p><ul><li><p><a href="https://github.com/ChartsOrg/Charts">MPAndroidChart</a> port, which is now called DGCharts because of name conflict with native one</p></li><li><p><a href="https://github.com/AppPear/ChartView">SwiftUICharts</a> for easy implementation</p></li><li><p><a href="https://github.com/AAChartModel/AAChartKit-Swift">AAChartKit-Swift</a> and it&#8217;s still very popular and as you might notice that it&#8217;s a port of Android AAChartKit</p></li><li><p><a href="https://github.com/ivnsch/SwiftCharts">SwiftCharts</a>, without maintainer now for months</p></li></ul><p>As you can see, Android chart libraries (even though they&#8217;re custom components) inspired a lot of these solutions. Many popular apps used the mentioned ports. What can I say: a very popular and famous companies have gained their users by implementing the Chart Component and sell it. Not a secret, that TradingView is a leader in charting controls and have their performant <a href="https://www.tradingview.com/charting-library-docs/latest/ui_elements/Chart">implementation</a>.</p><p>The ice began to melt at WWDC22, when Apple introduced Swift Charts. Here is the <a href="https://developer.apple.com/videos/play/wwdc2022/10136/">session</a> to get you started and get a brief overview. For those who want an official docs:</p><ul><li><p><a href="https://developer.apple.com/documentation/charts">Charts Framework</a></p></li><li><p><a href="https://developer.apple.com/documentation/charts/creating-a-chart-using-swift-charts">Create Charts Tutorial</a></p></li></ul><p>That would be enough to get a basic knowledge about the Charts for us to start.</p><div><hr></div><h2>Plain Chart</h2><p>Let&#8217;s create a data array and scatter chart for it. We will show a daily chart of humidity.</p><pre><code><code>struct HumidityRate: Identifiable {
    let humidity: Double
    let date: Date

    var id: Date { date }
}</code></code></pre><p>For convenient testing, we can add an initializer with date offset and extension:</p><pre><code><code>struct HumidityRate: Identifiable {
    let humidity: Int
    let date: Date

    var id: Date { date }

    init(minutesOffset: Double, humidity: Int) {
        self.date = Date().addingTimeInterval(minutesOffset * 60)
        self.humidity = humidity
    }
}

extension HumidityRate {
    static var samples: [HumidityRate] {
        [
            .init(minutesOffset: -3, humidity: 10),
            .init(minutesOffset: -2, humidity: 10),
            .init(minutesOffset: -1, humidity: 10),
            .init(minutesOffset: 0, humidity: 20),
            .init(minutesOffset: 1, humidity: 30),
            .init(minutesOffset: 2, humidity: 40),
            .init(minutesOffset: 3, humidity: 50),
            .init(minutesOffset: 4, humidity: 40),
            .init(minutesOffset: 5, humidity: 30),
            .init(minutesOffset: 6, humidity: 20),
            .init(minutesOffset: 7, humidity: 10)
        ]
    }
}</code></code></pre><p>Now it&#8217;s time for the chart. Construction is pretty simple. At least for now ) We will pass data and use <code>LineMark</code> to draw a line and <code>PointMark</code> to place dots.</p><pre><code><code>struct HumidityChartViewDemo: View {
    
    let data: [HumidityRate]
    
    var body: some View {
        Chart(data, id: \.date) { rate  in
            LineMark(
                x: .value(&#8221;&#8220;, rate.date),
                y: .value(&#8221;&#8220;, rate.humidity)
            )
            
            PointMark(
                x: .value(&#8221;&#8220;, rate.date),
                y: .value(&#8221;&#8220;, rate.humidity)
            )
        }
        .frame(height: 400)
        .padding()
    }
}

#Preview {
    let data: [HumidityRate] = HumidityRate.samples
    HumidityChartView(data: data)
}</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!go7m!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f66be4b-941e-49d8-988a-48dcd6a239b9_584x656.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!go7m!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f66be4b-941e-49d8-988a-48dcd6a239b9_584x656.png 424w, https://substackcdn.com/image/fetch/$s_!go7m!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f66be4b-941e-49d8-988a-48dcd6a239b9_584x656.png 848w, https://substackcdn.com/image/fetch/$s_!go7m!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f66be4b-941e-49d8-988a-48dcd6a239b9_584x656.png 1272w, https://substackcdn.com/image/fetch/$s_!go7m!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f66be4b-941e-49d8-988a-48dcd6a239b9_584x656.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!go7m!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f66be4b-941e-49d8-988a-48dcd6a239b9_584x656.png" width="386" height="433.5890410958904" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4f66be4b-941e-49d8-988a-48dcd6a239b9_584x656.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:656,&quot;width&quot;:584,&quot;resizeWidth&quot;:386,&quot;bytes&quot;:41238,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/179848950?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f66be4b-941e-49d8-988a-48dcd6a239b9_584x656.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!go7m!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f66be4b-941e-49d8-988a-48dcd6a239b9_584x656.png 424w, https://substackcdn.com/image/fetch/$s_!go7m!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f66be4b-941e-49d8-988a-48dcd6a239b9_584x656.png 848w, https://substackcdn.com/image/fetch/$s_!go7m!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f66be4b-941e-49d8-988a-48dcd6a239b9_584x656.png 1272w, https://substackcdn.com/image/fetch/$s_!go7m!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f66be4b-941e-49d8-988a-48dcd6a239b9_584x656.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Plain chart with line and dots</figcaption></figure></div><p>Isn&#8217;t it simple? Yes! Informative - not really ) We can start with colors which are very handy to highlight the values. Values before the current time would have smaller weight and opacity. And smooth interpolation for edges polishing. Which will lead to an interesting turnaround later.</p><pre><code><code>struct HumidityChartView: View {
    let data: [HumidityRate]
    
    var body: some View {
        Chart(data, id: \.date) { rate  in
            LineMark(
                x: .value(&#8221;&#8220;, rate.date),
                y: .value(&#8221;&#8220;, rate.humidity)
            )
            .interpolationMethod(.catmullRom)
            .foregroundStyle(LinearGradient(colors: data.compactMap({Color.colorForIndex($0.humidity)}), startPoint: .leading, endPoint: .trailing))
            
            PointMark(
                x: .value(&#8221;&#8220;, rate.date),
                y: .value(&#8221;&#8220;, rate.humidity)
            )
            .foregroundStyle(Color.colorForIndex(rate.humidity))
            .annotation(position: .top) {
                Text(&#8221;\(rate.humidity)&#8221;)
                    .font(.headline)
                    .fontWeight( rate.date &lt; Date() ? .regular: .bold)
                    .opacity( rate.date &lt; Date() ? 0.5 : 1.0)
            }
        }
        .frame(height: 400)
        .padding()
    }
}

extension Color {
    static func colorForIndex(_ humidity: Int) -&gt; Color {
        switch humidity {
        case 0..&lt;20: return .green
        case 20..&lt;50: return .yellow
        case 50..&lt;70: return .orange
        case 70...80: return .red
        default: return .purple
        }
    }
}</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!U0lN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc2ab5f4-5639-4d07-89dd-5dcd87fb940f_588x680.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!U0lN!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc2ab5f4-5639-4d07-89dd-5dcd87fb940f_588x680.png 424w, https://substackcdn.com/image/fetch/$s_!U0lN!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc2ab5f4-5639-4d07-89dd-5dcd87fb940f_588x680.png 848w, https://substackcdn.com/image/fetch/$s_!U0lN!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc2ab5f4-5639-4d07-89dd-5dcd87fb940f_588x680.png 1272w, https://substackcdn.com/image/fetch/$s_!U0lN!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc2ab5f4-5639-4d07-89dd-5dcd87fb940f_588x680.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!U0lN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc2ab5f4-5639-4d07-89dd-5dcd87fb940f_588x680.png" width="388" height="448.7074829931973" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bc2ab5f4-5639-4d07-89dd-5dcd87fb940f_588x680.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:680,&quot;width&quot;:588,&quot;resizeWidth&quot;:388,&quot;bytes&quot;:54416,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/179848950?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc2ab5f4-5639-4d07-89dd-5dcd87fb940f_588x680.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!U0lN!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc2ab5f4-5639-4d07-89dd-5dcd87fb940f_588x680.png 424w, https://substackcdn.com/image/fetch/$s_!U0lN!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc2ab5f4-5639-4d07-89dd-5dcd87fb940f_588x680.png 848w, https://substackcdn.com/image/fetch/$s_!U0lN!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc2ab5f4-5639-4d07-89dd-5dcd87fb940f_588x680.png 1272w, https://substackcdn.com/image/fetch/$s_!U0lN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc2ab5f4-5639-4d07-89dd-5dcd87fb940f_588x680.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Chart with gradient line and colored dots</figcaption></figure></div><blockquote><p><code>LinearGradient</code> should have the same amount of points as data samples!</p></blockquote><p>All looks great. Perhaps filling the gap below the chart with <code>AreaMark</code> will bring more visibility.</p><pre><code><code>struct HumidityChartView: View {
    
    let data: [HumidityRate]
    
    var body: some View {
        Chart(data, id: \.date) { rate  in
            //Area highlight
            AreaMark(x: .value(&#8221;&#8220;, rate.date),
                     y: .value(&#8221;&#8220;, rate.humidity))
            .foregroundStyle(LinearGradient(colors:
                                                data.compactMap({
                Color.colorForIndex($0.humidity)
            }),
                                            startPoint: .leading,
                                            endPoint: .trailing))
            .interpolationMethod(.catmullRom)
            
            LineMark(
                x: .value(&#8221;&#8220;, rate.date),
                y: .value(&#8221;&#8220;, rate.humidity)
            )
            .interpolationMethod(.catmullRom)
            .foregroundStyle(LinearGradient(colors: data.compactMap({Color.colorForIndex($0.humidity)}), startPoint: .leading, endPoint: .trailing))
            
            PointMark(
                x: .value(&#8221;&#8220;, rate.date),
                y: .value(&#8221;&#8220;, rate.humidity)
            )
            .foregroundStyle(Color.colorForIndex(rate.humidity))
            .annotation(position: .top) {
                Text(&#8221;\(rate.humidity)&#8221;)
                    .font(.headline)
                    .fontWeight( rate.date &lt; Date() ? .regular: .bold)
                    .opacity( rate.date &lt; Date() ? 0.5 : 1.0)
            }
        }
        .frame(height: 400)
        .padding()
    }
} </code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!QhON!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3ab602d-78ab-4167-9845-d0600a63c367_574x658.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!QhON!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3ab602d-78ab-4167-9845-d0600a63c367_574x658.png 424w, https://substackcdn.com/image/fetch/$s_!QhON!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3ab602d-78ab-4167-9845-d0600a63c367_574x658.png 848w, https://substackcdn.com/image/fetch/$s_!QhON!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3ab602d-78ab-4167-9845-d0600a63c367_574x658.png 1272w, https://substackcdn.com/image/fetch/$s_!QhON!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3ab602d-78ab-4167-9845-d0600a63c367_574x658.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!QhON!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3ab602d-78ab-4167-9845-d0600a63c367_574x658.png" width="382" height="437.9024390243902" data-attrs="{&quot;src&quot;:&quot;https://substackcdn.com/image/fetch/$s_!QhON!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3ab602d-78ab-4167-9845-d0600a63c367_574x658.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:437.9024390243902,&quot;width&quot;:382,&quot;resizeWidth&quot;:382,&quot;bytes&quot;:100940,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/179848950?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3ab602d-78ab-4167-9845-d0600a63c367_574x658.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!QhON!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3ab602d-78ab-4167-9845-d0600a63c367_574x658.png 424w, https://substackcdn.com/image/fetch/$s_!QhON!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3ab602d-78ab-4167-9845-d0600a63c367_574x658.png 848w, https://substackcdn.com/image/fetch/$s_!QhON!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3ab602d-78ab-4167-9845-d0600a63c367_574x658.png 1272w, https://substackcdn.com/image/fetch/$s_!QhON!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3ab602d-78ab-4167-9845-d0600a63c367_574x658.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Chart with interpolated line, points and area mark</figcaption></figure></div><p>Unfortunately, that merged the points with <code>AreaMark</code>. This can be fixed by adding extra annotation to the <code>PointMark:</code></p><pre><code><code>.annotation(position: .automatic, alignment: .center, spacing: -9.0) {
    Circle()
         .stroke(Color.black.opacity(0.5), lineWidth: 1)
}</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6d1b!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1a9802-9041-4609-af97-2dbbe7961ba1_582x638.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6d1b!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1a9802-9041-4609-af97-2dbbe7961ba1_582x638.png 424w, https://substackcdn.com/image/fetch/$s_!6d1b!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1a9802-9041-4609-af97-2dbbe7961ba1_582x638.png 848w, https://substackcdn.com/image/fetch/$s_!6d1b!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1a9802-9041-4609-af97-2dbbe7961ba1_582x638.png 1272w, https://substackcdn.com/image/fetch/$s_!6d1b!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1a9802-9041-4609-af97-2dbbe7961ba1_582x638.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6d1b!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1a9802-9041-4609-af97-2dbbe7961ba1_582x638.png" width="382" height="418.7560137457045" data-attrs="{&quot;src&quot;:&quot;https://substackcdn.com/image/fetch/$s_!6d1b!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1a9802-9041-4609-af97-2dbbe7961ba1_582x638.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:418.7560137457045,&quot;width&quot;:382,&quot;resizeWidth&quot;:382,&quot;bytes&quot;:105825,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/179848950?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1a9802-9041-4609-af97-2dbbe7961ba1_582x638.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!6d1b!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1a9802-9041-4609-af97-2dbbe7961ba1_582x638.png 424w, https://substackcdn.com/image/fetch/$s_!6d1b!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1a9802-9041-4609-af97-2dbbe7961ba1_582x638.png 848w, https://substackcdn.com/image/fetch/$s_!6d1b!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1a9802-9041-4609-af97-2dbbe7961ba1_582x638.png 1272w, https://substackcdn.com/image/fetch/$s_!6d1b!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e1a9802-9041-4609-af97-2dbbe7961ba1_582x638.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Now points are visible</figcaption></figure></div><div><hr></div><h2>Selection</h2><p>Starting from iOS 17, framework contains <a href="https://developer.apple.com/documentation/swiftui/view/chartxselection(range:)/">selection handling</a> for each of the axis.</p><pre><code><code>nonisolated public func chartXSelection&lt;P&gt;(value: Binding&lt;P?&gt;) -&gt; some View where P : Plottable</code></code></pre><p>Before that, we had to place an overlay and track which values were available at the exact location. For a small, not complicated selection it might work. By adding <code>RuleMark</code>, vertical line can be drawn with time for the selected X-value:</p><pre><code><code>struct HumidityChartViewDemo: View {
    
    @State private var selectedX: Date? = nil
    
    let data: [HumidityRate]
    
    var body: some View {
        Chart(data, id: \.date) { rate  in
            AreaMark(x: .value(&#8221;&#8220;, rate.date),
                     y: .value(&#8221;&#8220;, rate.humidity))
            .foregroundStyle(LinearGradient(colors:
                                                data.compactMap({
                Color.colorForIndex($0.humidity)
            }),
                                            startPoint: .leading,
                                            endPoint: .trailing))
            .interpolationMethod(.catmullRom)
            
            LineMark(
                x: .value(&#8221;&#8220;, rate.date),
                y: .value(&#8221;&#8220;, rate.humidity)
            )
            .interpolationMethod(.catmullRom)
            .foregroundStyle(LinearGradient(colors: data.compactMap({Color.colorForIndex($0.humidity)}), startPoint: .leading, endPoint: .trailing))
            
            PointMark(
                x: .value(&#8221;&#8220;, rate.date),
                y: .value(&#8221;&#8220;, rate.humidity)
            )
            .foregroundStyle(Color.colorForIndex(rate.humidity))
            .annotation(position: .top) {
                Text(&#8221;\(rate.humidity)&#8221;)
                    .font(.headline)
                    .fontWeight( rate.date &lt; Date() ? .regular: .bold)
                    .opacity( rate.date &lt; Date() ? 0.5 : 1.0)
            }.annotation(position: .automatic, alignment: .center, spacing: -9.0) {
                Circle()
                    .stroke(Color.black.opacity(0.5), lineWidth: 1)
            }
            
            if let selectedX {
                RuleMark(x: .value(&#8221;Selected&#8221;, selectedX))
                    .annotation(position: .automatic) {
                        VStack(spacing: 0) {
                            Text(selectedX, style: .time)
                        }
                    }
                
            }
        }
        .chartXSelection(value: $selectedX)
        .frame(height: 400)
        .padding()
    }
}</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!a4K-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc5a79f69-015b-47a6-8414-7620cefa3ee5_592x682.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!a4K-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc5a79f69-015b-47a6-8414-7620cefa3ee5_592x682.png 424w, https://substackcdn.com/image/fetch/$s_!a4K-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc5a79f69-015b-47a6-8414-7620cefa3ee5_592x682.png 848w, https://substackcdn.com/image/fetch/$s_!a4K-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc5a79f69-015b-47a6-8414-7620cefa3ee5_592x682.png 1272w, https://substackcdn.com/image/fetch/$s_!a4K-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc5a79f69-015b-47a6-8414-7620cefa3ee5_592x682.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!a4K-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc5a79f69-015b-47a6-8414-7620cefa3ee5_592x682.png" width="382" height="440.0743243243243" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c5a79f69-015b-47a6-8414-7620cefa3ee5_592x682.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:682,&quot;width&quot;:592,&quot;resizeWidth&quot;:382,&quot;bytes&quot;:100017,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/179848950?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc5a79f69-015b-47a6-8414-7620cefa3ee5_592x682.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!a4K-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc5a79f69-015b-47a6-8414-7620cefa3ee5_592x682.png 424w, https://substackcdn.com/image/fetch/$s_!a4K-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc5a79f69-015b-47a6-8414-7620cefa3ee5_592x682.png 848w, https://substackcdn.com/image/fetch/$s_!a4K-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc5a79f69-015b-47a6-8414-7620cefa3ee5_592x682.png 1272w, https://substackcdn.com/image/fetch/$s_!a4K-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc5a79f69-015b-47a6-8414-7620cefa3ee5_592x682.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Selection with RuleMark</figcaption></figure></div><p>Also, this recalculates the Y-axis to show selection more clearly. To remove it, we can strict the domain (by calculating max + 10 on Y-axis):</p><pre><code><code>.chartYScale(domain: 0...(min(100, data.map(\.humidity).max() ?? 0) + 10))</code></code></pre><p>We are moving pretty well! Fixing axis labels would be a good milestone, don&#8217;t you think? Right now we can&#8217;t even tell what time is it.</p><div><hr></div><h2>Axis Labels</h2><p>First, we need to show the date values. Time earlier than current should have lower opacity. For this, we need to use <code>chartXAxis</code>. According to <a href="https://developer.apple.com/documentation/swiftui/view/chartxaxis(_:)/">docs</a>, it used for configuring the X-axis with <code>ChartAxisContent</code>.</p><pre><code><code>AxisMarks(
    position: .bottom, values: data.compactMap(\.date)
){ value in
    if let date = value.as(Date.self) {
        AxisGridLine(stroke: .init(lineWidth: 1))
        
        AxisValueLabel {
            VStack(alignment: .center) {
                Text(date, format: .dateTime.hour().minute())
                    .font(.footnote)
                    .opacity( date &lt; Date() ? 0.5 : 1.0)
            }
        }
        
    }
}</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!s_07!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4cbb07e2-61a3-4e0a-b1b0-7ce0c4bf5c36_592x682.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!s_07!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4cbb07e2-61a3-4e0a-b1b0-7ce0c4bf5c36_592x682.png 424w, https://substackcdn.com/image/fetch/$s_!s_07!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4cbb07e2-61a3-4e0a-b1b0-7ce0c4bf5c36_592x682.png 848w, https://substackcdn.com/image/fetch/$s_!s_07!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4cbb07e2-61a3-4e0a-b1b0-7ce0c4bf5c36_592x682.png 1272w, https://substackcdn.com/image/fetch/$s_!s_07!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4cbb07e2-61a3-4e0a-b1b0-7ce0c4bf5c36_592x682.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!s_07!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4cbb07e2-61a3-4e0a-b1b0-7ce0c4bf5c36_592x682.png" width="382" height="440.0743243243243" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4cbb07e2-61a3-4e0a-b1b0-7ce0c4bf5c36_592x682.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:682,&quot;width&quot;:592,&quot;resizeWidth&quot;:382,&quot;bytes&quot;:117784,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/179848950?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4cbb07e2-61a3-4e0a-b1b0-7ce0c4bf5c36_592x682.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!s_07!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4cbb07e2-61a3-4e0a-b1b0-7ce0c4bf5c36_592x682.png 424w, https://substackcdn.com/image/fetch/$s_!s_07!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4cbb07e2-61a3-4e0a-b1b0-7ce0c4bf5c36_592x682.png 848w, https://substackcdn.com/image/fetch/$s_!s_07!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4cbb07e2-61a3-4e0a-b1b0-7ce0c4bf5c36_592x682.png 1272w, https://substackcdn.com/image/fetch/$s_!s_07!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4cbb07e2-61a3-4e0a-b1b0-7ce0c4bf5c36_592x682.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">X-axis labels added</figcaption></figure></div><p>Well, that is not ideal. Sometimes too many info is not needed. Showing only even dates and skip last date. Small extension will help us.</p><pre><code><code>extension Array {
    func evenIndexed() -&gt; Self {
        self.enumerated()
            .compactMap { index, element in
                index.isMultiple(of: 2) ? element : nil
            }
    }
}

.chartXAxis(content: {
        AxisMarks(
            position: .bottom, values: data.compactMap(\.date)
        ){ value in
            if let date = value.as(Date.self) {
                AxisGridLine(stroke: .init(lineWidth: 1))
                if data.compactMap(\.date).evenIndexed().contains(date) &amp;&amp; data.compactMap(\.date).last != date {
                    AxisValueLabel {
                        VStack(alignment: .center) {
                            Text(date, format: .dateTime.hour().minute())
                                .font(.footnote)
                                .opacity( date &lt; Date() ? 0.5 : 1.0)
                        }
                    }
               }
            }
        }
    })</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!d1mY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf53d395-7a9a-48a5-a3c0-9202c9467452_592x682.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!d1mY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf53d395-7a9a-48a5-a3c0-9202c9467452_592x682.png 424w, https://substackcdn.com/image/fetch/$s_!d1mY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf53d395-7a9a-48a5-a3c0-9202c9467452_592x682.png 848w, https://substackcdn.com/image/fetch/$s_!d1mY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf53d395-7a9a-48a5-a3c0-9202c9467452_592x682.png 1272w, https://substackcdn.com/image/fetch/$s_!d1mY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf53d395-7a9a-48a5-a3c0-9202c9467452_592x682.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!d1mY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf53d395-7a9a-48a5-a3c0-9202c9467452_592x682.png" width="382" height="440.0743243243243" data-attrs="{&quot;src&quot;:&quot;https://substackcdn.com/image/fetch/$s_!d1mY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf53d395-7a9a-48a5-a3c0-9202c9467452_592x682.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:440.0743243243243,&quot;width&quot;:382,&quot;resizeWidth&quot;:382,&quot;bytes&quot;:116087,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/179848950?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf53d395-7a9a-48a5-a3c0-9202c9467452_592x682.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!d1mY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf53d395-7a9a-48a5-a3c0-9202c9467452_592x682.png 424w, https://substackcdn.com/image/fetch/$s_!d1mY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf53d395-7a9a-48a5-a3c0-9202c9467452_592x682.png 848w, https://substackcdn.com/image/fetch/$s_!d1mY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf53d395-7a9a-48a5-a3c0-9202c9467452_592x682.png 1272w, https://substackcdn.com/image/fetch/$s_!d1mY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf53d395-7a9a-48a5-a3c0-9202c9467452_592x682.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Correct X-axis labels</figcaption></figure></div><p>Now we can get humidity axis. As with X-axis, without iterating over values - nothing will work. Humidity values can be calculated with this formula:</p><pre><code><code>Array(stride(from: 0, to: min(100, (data.map(\.humidity).max() ?? 0) + 10), by: 10)</code></code></pre><pre><code><code>
.chartYAxis {
    AxisMarks(
        position: .trailing,
        values: Array(
            stride(
                from: 0,
                to: min(100, (data.map(\.humidity).max() ?? 0) + 10),
                by: 10
            )
        )
    ) { value in
        if let number = value.as(Int.self) {
            // 20 and 50 are border values to highlight
            if [20, 50].contains(number) {
                AxisGridLine(stroke: .init(lineWidth: 2))
                    .foregroundStyle(Color.colorForIndex(number))

                AxisValueLabel {
                    VStack(alignment: .leading) {
                        Text(&#8221;\(number)&#8221;)
                            .fontWeight(.bold)
                    }
                }
                .foregroundStyle(Color.colorForIndex(number))

            } else {
                AxisGridLine(stroke: .init(lineWidth: 1))

                AxisValueLabel {
                    VStack(alignment: .leading) {
                        Text(&#8221;\(number)&#8221;)
                    }
                }
            }
        }
    }
}</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!XiS3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd77548dd-8e6f-4a9f-9e68-dd1af8018e59_592x682.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!XiS3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd77548dd-8e6f-4a9f-9e68-dd1af8018e59_592x682.png 424w, https://substackcdn.com/image/fetch/$s_!XiS3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd77548dd-8e6f-4a9f-9e68-dd1af8018e59_592x682.png 848w, https://substackcdn.com/image/fetch/$s_!XiS3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd77548dd-8e6f-4a9f-9e68-dd1af8018e59_592x682.png 1272w, https://substackcdn.com/image/fetch/$s_!XiS3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd77548dd-8e6f-4a9f-9e68-dd1af8018e59_592x682.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!XiS3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd77548dd-8e6f-4a9f-9e68-dd1af8018e59_592x682.png" width="382" height="440.0743243243243" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d77548dd-8e6f-4a9f-9e68-dd1af8018e59_592x682.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:682,&quot;width&quot;:592,&quot;resizeWidth&quot;:382,&quot;bytes&quot;:118379,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/179848950?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd77548dd-8e6f-4a9f-9e68-dd1af8018e59_592x682.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!XiS3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd77548dd-8e6f-4a9f-9e68-dd1af8018e59_592x682.png 424w, https://substackcdn.com/image/fetch/$s_!XiS3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd77548dd-8e6f-4a9f-9e68-dd1af8018e59_592x682.png 848w, https://substackcdn.com/image/fetch/$s_!XiS3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd77548dd-8e6f-4a9f-9e68-dd1af8018e59_592x682.png 1272w, https://substackcdn.com/image/fetch/$s_!XiS3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd77548dd-8e6f-4a9f-9e68-dd1af8018e59_592x682.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">New Y-axis with border values highlight</figcaption></figure></div><p>This is a great foundation for our upcoming styling and research. The next post will cover selection tweaks. You will find why did we use interpolation after all.</p><p><a href="https://gist.github.com/lanserxt/c8281540ccf94433b2a8a74168c8f651">Code for this part</a></p><p>Thanks for reading and stay tuned for the next part!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Start building with Swift and SwiftUI - Code-Along Notes and Q&A]]></title><description><![CDATA[Open the development world with Swift]]></description><link>https://antongubarenko.substack.com/p/start-building-with-swift-and-swiftui</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/start-building-with-swift-and-swiftui</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Tue, 18 Nov 2025 11:42:28 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/2af7376f-711e-41a0-adfc-132c378dba66_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Apple&#8217;s educational sessions continues with a new <strong><a href="https://www.youtube.com/watch?v=XapwQYZwmic">&#8220;Code-along: Start building with Swift and SwiftUI | Meet with Apple&#8220;</a>  </strong>video targeted mainly for beginner engineers which are just starting to work with Swift and SwiftUI as a framework, but it might be a good peek and generally a &#8220;development process&#8221; vision from Apple.</p><p>The code-along walks through building a basic app from scratch and using different frameworks (Photos, SwiftData, etc.). A pretty wide range, you might say? Yes! What normally takes months is condensed into 1h 30m, with the video presented alongside the editor view.</p><p>Usually, I don&#8217;t cover what&#8217;s happening on screen during these sessions. However, because the main audience here is beginner Swift developers, asking specific questions in such a &#8220;whole-in-one&#8221; session is challenging. This leads to a smaller number of concrete questions &#8212; and even fewer detailed answers. Still, I&#8217;ll include a Q&amp;A summary in the last chapter, because some of them are pretty interesting and direct. For now, here are the main takeaways&#8230;</p><div><hr></div><h2>Don&#8217;t Skip Tutorials</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lFgR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09ddc990-6f0d-4fc7-8f5f-47121ef9b4ac_2024x756.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lFgR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09ddc990-6f0d-4fc7-8f5f-47121ef9b4ac_2024x756.png 424w, https://substackcdn.com/image/fetch/$s_!lFgR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09ddc990-6f0d-4fc7-8f5f-47121ef9b4ac_2024x756.png 848w, https://substackcdn.com/image/fetch/$s_!lFgR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09ddc990-6f0d-4fc7-8f5f-47121ef9b4ac_2024x756.png 1272w, https://substackcdn.com/image/fetch/$s_!lFgR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09ddc990-6f0d-4fc7-8f5f-47121ef9b4ac_2024x756.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lFgR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09ddc990-6f0d-4fc7-8f5f-47121ef9b4ac_2024x756.png" width="1456" height="544" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/09ddc990-6f0d-4fc7-8f5f-47121ef9b4ac_2024x756.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:544,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1101672,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/178816586?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09ddc990-6f0d-4fc7-8f5f-47121ef9b4ac_2024x756.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!lFgR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09ddc990-6f0d-4fc7-8f5f-47121ef9b4ac_2024x756.png 424w, https://substackcdn.com/image/fetch/$s_!lFgR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09ddc990-6f0d-4fc7-8f5f-47121ef9b4ac_2024x756.png 848w, https://substackcdn.com/image/fetch/$s_!lFgR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09ddc990-6f0d-4fc7-8f5f-47121ef9b4ac_2024x756.png 1272w, https://substackcdn.com/image/fetch/$s_!lFgR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09ddc990-6f0d-4fc7-8f5f-47121ef9b4ac_2024x756.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Courtesy of Apple Inc.</figcaption></figure></div><p>Five years ago, we could only dream of having official tutorials from Apple. The whole community grew up on self-taught articles and videos. Not bad, but not ideal &#8212; especially considering Apple clearly has the resources and people to create proper learning materials.</p><p><a href="https://developer.apple.com/tutorials/develop-in-swift/">Develop in Swift Tutorials</a> are interactive, detailed, and include step-by-step guidance with source code for each section. They even cover design, distribution, and other parts of app development. You can move at your own pace, follow your own schedule, and return to any part whenever you need.</p><div><hr></div><h2>Meet with Apple Sessions</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_qh7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F306a3b41-179c-41c0-82ec-8bdd9f5f7f65_2006x1058.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_qh7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F306a3b41-179c-41c0-82ec-8bdd9f5f7f65_2006x1058.png 424w, https://substackcdn.com/image/fetch/$s_!_qh7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F306a3b41-179c-41c0-82ec-8bdd9f5f7f65_2006x1058.png 848w, https://substackcdn.com/image/fetch/$s_!_qh7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F306a3b41-179c-41c0-82ec-8bdd9f5f7f65_2006x1058.png 1272w, https://substackcdn.com/image/fetch/$s_!_qh7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F306a3b41-179c-41c0-82ec-8bdd9f5f7f65_2006x1058.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_qh7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F306a3b41-179c-41c0-82ec-8bdd9f5f7f65_2006x1058.png" width="1456" height="768" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/306a3b41-179c-41c0-82ec-8bdd9f5f7f65_2006x1058.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:768,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2754984,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/178816586?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F306a3b41-179c-41c0-82ec-8bdd9f5f7f65_2006x1058.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!_qh7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F306a3b41-179c-41c0-82ec-8bdd9f5f7f65_2006x1058.png 424w, https://substackcdn.com/image/fetch/$s_!_qh7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F306a3b41-179c-41c0-82ec-8bdd9f5f7f65_2006x1058.png 848w, https://substackcdn.com/image/fetch/$s_!_qh7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F306a3b41-179c-41c0-82ec-8bdd9f5f7f65_2006x1058.png 1272w, https://substackcdn.com/image/fetch/$s_!_qh7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F306a3b41-179c-41c0-82ec-8bdd9f5f7f65_2006x1058.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Courtesy of Apple Inc.</figcaption></figure></div><p>If you&#8217;ve been following me for a while, you might have noticed that this is a new format &#8212; something that stands out from what Apple has done before. The official YouTube channel only appeared a few years ago, letting us finally browse both new and old WWDC videos there. Previously, the Apple Developer app was the only option.</p><p>Take your time to register there: <a href="https://developer.apple.com/events/">Available Sessions</a></p><div><hr></div><h2>Join the Swift Student Challenge</h2><p>The <a href="https://developer.apple.com/swift-student-challenge/">Swift Student Challenge</a> is an annual competition hosted by Apple that encourages students 13 years or older to create an interactive and creative project using Swift Playgrounds. The goal is to create an experience that can be enjoyed in about three minutes. It&#8217;s not about building a massive, complex application, but rather showcasing creativity, innovation, and originality.</p><p>How the Swift Student Challenge helps beginner developers?</p><h4><strong>A Great Learning Opportunity</strong></h4><ul><li><p><strong>Hands-on Experience:</strong> The challenge provides a practical way to apply coding skills to a real project. This is a fantastic way to move from theoretical knowledge to practical application.</p></li><li><p><strong>Mastering Swift:</strong> Even if you&#8217;re new to Swift, the challenge is a great way to learn the language. Apple provides resources like the Swift Playgrounds app, which is designed for beginners to learn coding.</p></li><li><p><strong>Focus on Creativity:</strong> The challenge emphasizes creativity and problem-solving over pure technical complexity. This allows beginners to focus on a great idea and execute it well, without needing to be an expert in every aspect of Swift development.</p></li></ul><h4><strong>Building a Portfolio and Gaining Recognition</strong></h4><ul><li><p><strong>Showcase Your Skills:</strong> Participating in the challenge, and especially winning, is a great addition to a developer&#8217;s portfolio. It demonstrates initiative, technical skills, and creative problem-solving abilities that are highly valued by potential employers and college admissions committees.</p></li><li><p><strong>Industry Recognition:</strong> Winners receive recognition from Apple, which can be a significant boost to a young developer&#8217;s career. This can lead to internships and job opportunities at major tech companies.</p></li></ul><h4><strong>Networking and Community</strong></h4><ul><li><p><strong>Connect with Peers:</strong> The challenge connects you with a global community of student developers. You can share ideas, get feedback, and build friendships with like-minded individuals.</p></li><li><p><strong>Access to Apple&#8217;s Developer Network:</strong> Winners gain access to Apple&#8217;s global developer network, providing opportunities to connect with industry professionals and mentors.</p></li></ul><p>Submissions open <strong>February 6-28, 2026</strong>.</p><div><hr></div><h2>Q&amp;A</h2><p>Finally, sharing the sessions summary.</p><h3>What is the current structure of the tutorials? I see SwiftUI tutorials, app development tutorials, and the new Develop in Swift tutorials.</h3><blockquote><p>You can find all the tutorials at: <a href="https://developer.apple.com/tutorials/develop-in-swift/">link</a>. The tutorials we are following today are under <strong>App Development</strong>.</p></blockquote><p></p><h3>I&#8217;m a visual thinker&#8212;are there learning resources that help bridge the gap between art tools and code?</h3><blockquote><p>The <strong><a href="https://developer.apple.com/design/human-interface-guidelines/designing-for-visionos">Designing for visionOS</a></strong> section in the Human Interface Guidelines is excellent.</p></blockquote><p></p><h3>Can accent colors change based on the system&#8217;s light or dark appearance?</h3><blockquote><p>You can change the accent color in your Asset Catalog. By doing this, you can set a color for light appearance and another for dark appearance if you wish.</p></blockquote><p></p><h3>How can we find our code snippets in the new Xcode UI?</h3><blockquote><p>You can use <strong>Command + Shift + L</strong> to open the library. You will find your code snippets there.</p></blockquote><p></p><h3>Does SwiftData replace Core Data?</h3><blockquote><p>SwiftData doesn&#8217;t completely replace Core Data today. SwiftData&#8217;s default store uses Core Data, and Core Data provides more flexibility in some cases.</p></blockquote><p></p><h3>How does <code>@Observable</code> differ from <code>@ObservableObject</code> in terms of performance and boilerplate?</h3><blockquote><p>Starting with iOS 17, we recommend using the <strong>Observable macro</strong>. Adopting Observation provides these benefits:</p><ul><li><p>Tracking optionals and collections of objects, which isn&#8217;t possible with ObservableObject.</p></li><li><p>Updating views only when properties read by the view change, improving performance.</p><p><a href="https://developer.apple.com/documentation/SwiftUI/Migrating-from-the-observable-object-protocol-to-the-observable-macro">Check out</a></p></li></ul></blockquote><p></p><h3>I see the benefit of copying and pasting from the Swift tutorial to set up the project, but is there guidance about using coding assistants to set up and continue a project?</h3><blockquote><p>Coding assistant is a great tool. Also, join the Meet with Apple <a href="https://developer.apple.com/events/view/KT5L4XTWKQ/dashboard">session on </a><strong><a href="https://developer.apple.com/events/view/KT5L4XTWKQ/dashboard">February 5th</a></strong>, where you can follow along with an Apple expert and experiment with the latest Coding Intelligence tools in Xcode 26.</p></blockquote><p></p><h3>What is UIImage?</h3><blockquote><p>A UIImage object manages image data. You use image objects to represent image data of all kinds, and the class handles all formats supported by UIKit. <a href="https://developer.apple.com/documentation/uikit/uiimage">Apple Docs.</a></p></blockquote><p></p><h3>Why do some modifiers, like <code>.scrollDismissesKeyboard()</code>, go <strong>outside</strong> the ScrollView, while modifiers like <code>.navigationTitle() </code>go <strong>inside</strong> the NavigationStack? What&#8217;s the difference?</h3><blockquote><p>Excellent question! View modifiers (e.g., <code>.scrollDismissesKeyboard()</code>) modify the view itself and must be applied to the view. Environment modifiers (e.g., <code>.navigationTitle()</code>) modify views within a container&#8217;s context and therefore go inside the container.</p></blockquote><p></p><h3>I&#8217;m watching from Windows and normally code in React. I want to understand Swift conceptually so I can rebuild this app in my own stack. What patterns should I pay closest attention to?</h3><blockquote><p><a href="https://developer.apple.com/swift/get-started/">Swift Pathway</a> and the <a href="https://www.swift.org/getting-started/">Getting Started guide</a> are great resources.</p></blockquote><p></p><h3>Are visionOS apps accepted for the Swift Student Challenge?</h3><blockquote><p>For Swift Student Challenge 2026, <strong>app playgrounds</strong> are accepted. They can be built in Xcode or Swift Playgrounds, and must target <strong>iOS or iPadOS</strong>.</p></blockquote><p></p><h3>I want to participate in Swift Student Challenge 2026. Are we allowed to submit two ideas/apps?</h3><blockquote><p>Submit only <strong>one</strong> app. Visit the <a href="https://developer.apple.com/swift-student-challenge/eligibility/">eligibility page</a>.</p></blockquote><p></p><h3>Is it permitted for the Swift Student Challenge to include Apple&#8217;s Foundation Models framework in an app?</h3><blockquote><p>Yes &#8212; on-device Apple Intelligence frameworks and other Apple technologies may be used.</p></blockquote><p></p><h3>Why separate the contentStack into a variable instead of writing it inline in the body? Is there an advantage?</h3><blockquote><p>Extracting views in SwiftUI keeps your code organized, readable, and maintainable. In this project, it will grow in later sections, which is why it was extracted.</p></blockquote><p></p><h3>How can you pass a binding through a <code>.navigationDestination</code>, such as from a list view to a detail view?</h3><blockquote><p>Use <code>navigationDestination(item:destination:)</code>. When the item binding is non-nil, SwiftUI passes the value to the destination. <a href="https://developer.apple.com/documentation/swiftui/understanding-the-composition-of-navigation-stack">Learn more.</a></p></blockquote><div><hr></div><h1>Acknowledgments<strong> &#127942;</strong></h1><p>A big thank-you to everyone who joined and contributed thoughtful, insightful, and engaging questions throughout the session &#8212; your curiosity and participation made the discussion truly rich and collaborative.</p><p><strong>Special thanks to:</strong></p><p>Jay Zheng, Philippos Sidiroglou, Sasha Jarohevskii, Jonathan Judelson, Okba Khenissi, Jobie, Ayush J., Christopher State, Erik Jimenez, Derek Haugen, David Ram&#243;n Chica, Gina Mahaz, Nick, Roman Inderm&#252;hle, Tatiana Brimm, Evan S., Joe Heck, Ash, Steve Talkowski, Mansi Bansal, Kevin Johnson.</p><p>Finally, a heartfelt thank-you to the Apple team and moderators for leading the session, sharing expert guidance, and providing such clear explanations of app optimization techniques. Your contributions made this an exceptional learning experience for everyone involved.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h1>One more thing&#8230;</h1><p>Ever tried to explain &#8220;Yak Shaving,&#8221; &#8220;Spaghetti Code,&#8221; or &#8220;Imposter Syndrome&#8221;? Now you don&#8217;t have to &#8212; just <em>send a sticker</em>.</p><p><strong><a href="https://apps.apple.com/us/app/tectalk/id6745105035">TecTalk</a></strong> turns everyday developer slang into fun, relatable stickers for your chats. Whether you&#8217;re venting about bugs or celebrating a successful deploy, there&#8217;s a sticker for every tech mood.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!G70v!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!G70v!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png 424w, https://substackcdn.com/image/fetch/$s_!G70v!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png 848w, https://substackcdn.com/image/fetch/$s_!G70v!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png 1272w, https://substackcdn.com/image/fetch/$s_!G70v!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!G70v!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png" width="428" height="428" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1456,&quot;width&quot;:1456,&quot;resizeWidth&quot;:428,&quot;bytes&quot;:1355045,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/177634693?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!G70v!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png 424w, https://substackcdn.com/image/fetch/$s_!G70v!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png 848w, https://substackcdn.com/image/fetch/$s_!G70v!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png 1272w, https://substackcdn.com/image/fetch/$s_!G70v!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Created by me &#129489;&#8205;&#128187; &#8212; made <em>by a dev, for devs</em> &#8212; and <a href="https://apps.apple.com/us/app/tectalk/id6745105035">available now</a> at a <strong>very affordable price</strong>.</p><p>Express your inner techie. Stop typing. <a href="https://apps.apple.com/us/app/tectalk/id6745105035">Start sticking</a>.</p>]]></content:encoded></item><item><title><![CDATA[SwiftUI: Discardable Slider]]></title><description><![CDATA[Slider extension to track filter redundant changes]]></description><link>https://antongubarenko.substack.com/p/swiftui-discardable-slider</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swiftui-discardable-slider</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Tue, 11 Nov 2025 12:11:29 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/00949c1d-0f2d-4204-82cc-b6f5c9f434b1_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>While starting a new project, feature, or screen there is always a good chance to make it great in terms of functionality, architecture and UI right from the start. This sets the mood for further work and make us happy in the long run. Especially, when this approach will help to develop a robust and efficient components without profiling the entire application.</p><h2>Our Task</h2><blockquote><p>We need to make a <code>Slider</code> which will not instantly change the initial value, but only after some sort of confirmation. Why: slider triggers a change of event on each interaction and this change is linked to multiple events in Data Layer and Network layer. For example: each change might go to SwiftData/CoreData and re-trigger the change of other components which is redundant and not needed.</p></blockquote><p>Let&#8217;s start building it step by step! Or tick by tick) It&#8217;s a <code>Slider</code> after all. </p><p>Slider in SwiftUI is pretty straightforward component. From the first look. Let&#8217;s make a range from 0 to 10 with step 1 to pick the values in slider. Keep in mind, that it accepts only values conforming to <a href="https://developer.apple.com/documentation/swiftui/slider/init(value:in:oneditingchanged:)">BinaryFloatingPoint</a>: </p><pre><code>@State private var filterValue = 5.0
VStack {
    Text(&#8221;Values to filter&#8221;)
    
    Slider(value: $filterValue, in: 0...10, step: 1)
}.onChange(of: filterValue) {
    print(&#8221;Changed to \(filterValue)&#8221;)
}</code></pre><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!bkqD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d18e0d6-ebbe-4dd1-bed7-a2713cb3473e_532x124.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!bkqD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d18e0d6-ebbe-4dd1-bed7-a2713cb3473e_532x124.png 424w, https://substackcdn.com/image/fetch/$s_!bkqD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d18e0d6-ebbe-4dd1-bed7-a2713cb3473e_532x124.png 848w, https://substackcdn.com/image/fetch/$s_!bkqD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d18e0d6-ebbe-4dd1-bed7-a2713cb3473e_532x124.png 1272w, https://substackcdn.com/image/fetch/$s_!bkqD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d18e0d6-ebbe-4dd1-bed7-a2713cb3473e_532x124.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!bkqD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d18e0d6-ebbe-4dd1-bed7-a2713cb3473e_532x124.png" width="532" height="124" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2d18e0d6-ebbe-4dd1-bed7-a2713cb3473e_532x124.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:124,&quot;width&quot;:532,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:12039,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/178444389?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d18e0d6-ebbe-4dd1-bed7-a2713cb3473e_532x124.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!bkqD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d18e0d6-ebbe-4dd1-bed7-a2713cb3473e_532x124.png 424w, https://substackcdn.com/image/fetch/$s_!bkqD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d18e0d6-ebbe-4dd1-bed7-a2713cb3473e_532x124.png 848w, https://substackcdn.com/image/fetch/$s_!bkqD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d18e0d6-ebbe-4dd1-bed7-a2713cb3473e_532x124.png 1272w, https://substackcdn.com/image/fetch/$s_!bkqD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d18e0d6-ebbe-4dd1-bed7-a2713cb3473e_532x124.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a><figcaption class="image-caption">Plain Slider - No Ticks</figcaption></figure></div><div><hr></div><p>Looks good, but let&#8217;s add some ticks? There is such feature:</p><pre><code>Slider(value: $filterValue, in: 0...10, step: 1, label: {}, tick: {
    value in
    SliderTick(value, label: {
        Text(String(format: &#8220;%.0f&#8221;, value))
    })
})</code></pre><p>Unfortunately, this will <strong>work only for macOS</strong>. To add some basic automatic tick you need to pass label. It can be <code>Label</code> or <code>EmptyView</code>.</p><pre><code>VStack {
    Text(&#8221;Values to filter&#8221;)
    
    //Small change, yeah?
    Slider(value: $filterValue, in: 0...10, step: 1) {}
}.onChange(of: filterValue) {
    print(&#8221;Changed to \(filterValue)&#8221;)
}</code></pre><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Obrt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcb18ba4-ffb8-4811-b3c6-c61be2040d62_528x136.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Obrt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcb18ba4-ffb8-4811-b3c6-c61be2040d62_528x136.png 424w, https://substackcdn.com/image/fetch/$s_!Obrt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcb18ba4-ffb8-4811-b3c6-c61be2040d62_528x136.png 848w, https://substackcdn.com/image/fetch/$s_!Obrt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcb18ba4-ffb8-4811-b3c6-c61be2040d62_528x136.png 1272w, https://substackcdn.com/image/fetch/$s_!Obrt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcb18ba4-ffb8-4811-b3c6-c61be2040d62_528x136.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Obrt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcb18ba4-ffb8-4811-b3c6-c61be2040d62_528x136.png" width="528" height="136" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fcb18ba4-ffb8-4811-b3c6-c61be2040d62_528x136.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:136,&quot;width&quot;:528,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:13126,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/178444389?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcb18ba4-ffb8-4811-b3c6-c61be2040d62_528x136.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Obrt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcb18ba4-ffb8-4811-b3c6-c61be2040d62_528x136.png 424w, https://substackcdn.com/image/fetch/$s_!Obrt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcb18ba4-ffb8-4811-b3c6-c61be2040d62_528x136.png 848w, https://substackcdn.com/image/fetch/$s_!Obrt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcb18ba4-ffb8-4811-b3c6-c61be2040d62_528x136.png 1272w, https://substackcdn.com/image/fetch/$s_!Obrt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffcb18ba4-ffb8-4811-b3c6-c61be2040d62_528x136.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Plain Slider - with ticks</figcaption></figure></div><div><hr></div><p>Now we can see that our <code>onChange</code> is triggered all the time we change value. Let&#8217;s wrap this <code>Slider</code> into another <code>View</code>. Let&#8217;s call it <code>DiscardableSlider</code>:</p><pre><code>struct DiscardableSlider&lt;V: Strideable&gt;: View where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint {
    internal init(value: Binding&lt;V&gt;, bounds: ClosedRange&lt;V&gt;, step: V.Stride) {
        self._value = value
        self.bounds = bounds
        self.step = step
        self._pendingValue = State(initialValue: value.wrappedValue)
    }
    
    //Same parameters as native Slider 
    @Binding var value: V
    let bounds: ClosedRange&lt;V&gt;
    let step: V.Stride
    
    //Local value for inner state
    @State private var pendingValue: V
   
    var body: some View {
        VStack(spacing: 4.0) {
            Slider(value: $pendingValue, in: bounds, step:  step) {}
        }
    }
}</code></pre><div><hr></div><p>To make slider discardable we can add button below it. You might use <code>.overlay</code> to prevent size growth. In my case, height change is needed so putting the controls below the slider. I&#8217;m still experimenting with Liquid Glass (as all developers probably) and placed it inside a <code>GlassEffectContainer</code>.</p><pre><code>struct DiscardableSliderStep1&lt;V: Strideable&gt;: View where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint {
    internal init(value: Binding&lt;V&gt;, bounds: ClosedRange&lt;V&gt;, step: V.Stride) {
        self._value = value
        self.bounds = bounds
        self.step = step
        self._pendingValue = State(initialValue: value.wrappedValue)
    }
    
    //Same parameters as native Slider
    @Binding var value: V
    let bounds: ClosedRange&lt;V&gt;
    let step: V.Stride
    
    //Local value for inner state
    @State private var pendingValue: V
    
    
    var body: some View {
        VStack(spacing: 4.0) {
            Slider(value: $pendingValue, in: bounds, step:  step) {}
            
            //Discard panel
            HStack {
                Spacer()
                
                buttonsStack
                .padding(4)
            }
        }
    }

private var buttonsStack: some View {
    GlassEffectContainer {
        HStack(spacing: 12.0) {
            Button {
                withAnimation {
                    pendingValue = value
                }
            } label: {
                Label(&#8221;Cancel&#8221;, systemImage: &#8220;xmark.circle&#8221;)
                    .labelStyle(.iconOnly) // compact; keep just the icon if you prefer
                    .foregroundStyle(.red)
            }
            .frame(width: 40, height: 40)
            .buttonStyle(.glass)
            
            Button {
                value = pendingValue
            } label: {
                Label(&#8221;Save&#8221;, systemImage: &#8220;checkmark.circle.fill&#8221;)
                    .labelStyle(.iconOnly)
                    .font(.title3)
                    .foregroundStyle(.green)   // green checkmark
            }
            .frame(width: 40, height: 40)
            .buttonStyle(.glass)
        }
    }
  }
}</code></pre><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!S5Rv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F634288cc-4e5e-477e-acba-d7566fb67524_698x190.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!S5Rv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F634288cc-4e5e-477e-acba-d7566fb67524_698x190.png 424w, https://substackcdn.com/image/fetch/$s_!S5Rv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F634288cc-4e5e-477e-acba-d7566fb67524_698x190.png 848w, https://substackcdn.com/image/fetch/$s_!S5Rv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F634288cc-4e5e-477e-acba-d7566fb67524_698x190.png 1272w, https://substackcdn.com/image/fetch/$s_!S5Rv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F634288cc-4e5e-477e-acba-d7566fb67524_698x190.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!S5Rv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F634288cc-4e5e-477e-acba-d7566fb67524_698x190.png" width="524" height="142.63610315186247" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/634288cc-4e5e-477e-acba-d7566fb67524_698x190.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:190,&quot;width&quot;:698,&quot;resizeWidth&quot;:524,&quot;bytes&quot;:25081,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/178444389?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F634288cc-4e5e-477e-acba-d7566fb67524_698x190.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!S5Rv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F634288cc-4e5e-477e-acba-d7566fb67524_698x190.png 424w, https://substackcdn.com/image/fetch/$s_!S5Rv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F634288cc-4e5e-477e-acba-d7566fb67524_698x190.png 848w, https://substackcdn.com/image/fetch/$s_!S5Rv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F634288cc-4e5e-477e-acba-d7566fb67524_698x190.png 1272w, https://substackcdn.com/image/fetch/$s_!S5Rv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F634288cc-4e5e-477e-acba-d7566fb67524_698x190.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Slider with actions panel</figcaption></figure></div><p>What do we have here:</p><ul><li><p><em><strong>Cancel</strong> </em>button to discard slider value with animation</p></li><li><p><em><strong>Apply</strong></em> button to trigger update of initial value</p></li></ul><div><hr></div><p>Right now it&#8217;s visible all the time. Do we need to show it if the value hasn&#8217;t changed? Of course, <strong>no</strong>. By adding local animation variable which will trigger the panel or hide it after action.</p><pre><code>struct DiscardableSlider&lt;V: Strideable&gt;: View where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint {
    internal init(value: Binding&lt;V&gt;, bounds: ClosedRange&lt;V&gt;, step: V.Stride) {
        self._value = value
        self.bounds = bounds
        self.step = step
        self._pendingValue = State(initialValue: value.wrappedValue)
    }
    
    @Binding var value: V
    let bounds: ClosedRange&lt;V&gt;
    let step: V.Stride
    
    @State private var pendingValue: V
    
    @State private var showDiscard: Bool = false
    
    var body: some View {
        VStack(spacing: 4.0) {
            
            Slider(value: $pendingValue, in: bounds, step:  step) {}
                .onChange(of: pendingValue) {
                    withAnimation {
                        showDiscard = pendingValue == value
                    }
                }
            if showDiscard {
                HStack {
                    Spacer()
                    
                    buttonsStack
                    .padding(4)
                }
                .animation(.linear,value: showDiscard)
                .transition(.move(edge: !showDiscard ? .bottom : .top).combined(with: .opacity))
            }
        }
    }
    
    private var buttonsStack: some View {
        GlassEffectContainer {
            HStack(spacing: 12.0) {
                Button {
                    withAnimation {
                        pendingValue = value
                    }
                } label: {
                    Label(&#8221;Cancel&#8221;, systemImage: &#8220;xmark.circle&#8221;)
                        .labelStyle(.iconOnly) 
                        .foregroundStyle(.red)
                }
                .frame(width: 40, height: 40)
                .buttonStyle(.glass)
                
                Button {
                    value = pendingValue
                } label: {
                    Label(&#8221;Save&#8221;, systemImage: &#8220;checkmark.circle.fill&#8221;)
                        .labelStyle(.iconOnly)
                        .font(.title3)
                        .foregroundStyle(.green)
                }
                .frame(width: 40, height: 40)
                .buttonStyle(.glass)
            }
        }
    }
}</code></pre><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;67cd40fd-0517-43ee-a044-192d73988596&quot;,&quot;duration&quot;:null}"></div><p>Right now we have:</p><ul><li><p><code>withAnimation</code> wraps changes for animation variable</p></li><li><p>Panel visibility linked to that variable</p></li><li><p><code>transition</code> with combined animations depending on appearance logic</p></li></ul><p>However, you must have been seen a small glitch. Panel might not discard all the time on slider change. All lies in the value comparison:</p><pre><code>showDiscard = pendingValue == value</code></pre><p>You can&#8217;t compare float-point values like that. Counting accuracy between values is not just more precise - it&#8217;s the correct way:</p><pre><code>showDiscard = abs(pendingValue - value) &gt; 0.1</code></pre><div><hr></div><h1>Conclusion</h1><p>We&#8217;ve created a convenient wrapper around <code>Slider</code> to show actions panel and trigger initial value change only if needed. This approach will prevent redundant actions and storage overhead.</p><p><a href="https://gist.github.com/lanserxt/735c5c3d02c95a20d9fb26e9afaa1194">Source Code</a></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h1>One more thing&#8230;</h1><p>Ever tried to explain &#8220;Yak Shaving,&#8221; &#8220;Spaghetti Code,&#8221; or &#8220;Imposter Syndrome&#8221;? Now you don&#8217;t have to &#8212; just <em>send a sticker</em>.</p><p><strong><a href="https://apps.apple.com/us/app/tectalk/id6745105035">TecTalk</a></strong> turns everyday developer slang into fun, relatable stickers for your chats. Whether you&#8217;re venting about bugs or celebrating a successful deploy, there&#8217;s a sticker for every tech mood.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Epnp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5258c670-533b-4abb-a8a1-c279c69edf20_3960x6483.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Epnp!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5258c670-533b-4abb-a8a1-c279c69edf20_3960x6483.png 424w, https://substackcdn.com/image/fetch/$s_!Epnp!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5258c670-533b-4abb-a8a1-c279c69edf20_3960x6483.png 848w, https://substackcdn.com/image/fetch/$s_!Epnp!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5258c670-533b-4abb-a8a1-c279c69edf20_3960x6483.png 1272w, https://substackcdn.com/image/fetch/$s_!Epnp!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5258c670-533b-4abb-a8a1-c279c69edf20_3960x6483.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Epnp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5258c670-533b-4abb-a8a1-c279c69edf20_3960x6483.png" width="275" height="450.27472527472526" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5258c670-533b-4abb-a8a1-c279c69edf20_3960x6483.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:2384,&quot;width&quot;:1456,&quot;resizeWidth&quot;:275,&quot;bytes&quot;:1909025,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/178444389?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5258c670-533b-4abb-a8a1-c279c69edf20_3960x6483.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Epnp!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5258c670-533b-4abb-a8a1-c279c69edf20_3960x6483.png 424w, https://substackcdn.com/image/fetch/$s_!Epnp!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5258c670-533b-4abb-a8a1-c279c69edf20_3960x6483.png 848w, https://substackcdn.com/image/fetch/$s_!Epnp!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5258c670-533b-4abb-a8a1-c279c69edf20_3960x6483.png 1272w, https://substackcdn.com/image/fetch/$s_!Epnp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5258c670-533b-4abb-a8a1-c279c69edf20_3960x6483.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Created by me &#129489;&#8205;&#128187; &#8212; made <em>by a dev, for devs</em> &#8212; and <a href="https://apps.apple.com/us/app/tectalk/id6745105035">available now</a> at a <strong>very affordable price</strong>.</p><p>Express your inner techie. Stop typing. <a href="https://apps.apple.com/us/app/tectalk/id6745105035">Start sticking</a>.</p>]]></content:encoded></item><item><title><![CDATA[Optimize Your App’s Speed and Efficiency: Q&A]]></title><description><![CDATA[Learn depth of optimization from Apple Team]]></description><link>https://antongubarenko.substack.com/p/optimize-your-apps-speed-and-efficiency</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/optimize-your-apps-speed-and-efficiency</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Fri, 31 Oct 2025 13:06:44 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/096ee0b3-a113-4ee8-9c9c-3f95362260d3_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As you might now from my previous post &#8212; <a href="https://antongubarenko.substack.com/p/ios-26-foundation-model-framework-f6d">iOS 26: Foundation Model Framework - Code-Along Q&amp;A</a> &#8212; Apple is experimenting with new educational formats: webinars, 1-on-1 labs with engineers, and even hybrid sessions. Recently, this exact type of event was hosted in Cupertino and was also available online, where participants could once again ask questions and get responses directly from Apple engineers.</p><p>Today, I&#8217;m (continuing the format I started earlier) sharing questions from the &#8220;<a href="https://developer.apple.com/videos/play/meet-with-apple/212/">Optimize your app&#8217;s speed and efficiency</a>&#8221; session where online part consisted of:</p><ul><li><p>LiquidGlass containers: performance and usage</p></li><li><p>Battery performance examples</p></li><li><p>A deep dive into Foundation Model Framework Instruments tools</p></li><li><p>SwiftUI Performance</p></li><li><p>Guest Knowledge: Snap - shared how they organized performance tools</p></li></ul><blockquote><p><strong>Disclaimer:</strong> At the time of publishing, the session video is only available on <a href="https://www.youtube.com/watch?v=yXAQTIKR8fk&amp;t=538s">YouTube</a>&#8212; similar to how it was with the Foundation Models session.</p></blockquote><p>Everything is sorted, split into sections, and grammar-checked as usual.</p><p>Without further ado &#8212; the Q&amp;A below!</p><div><hr></div><h2>General Usage &#129470;</h2><h3>Is there a good pattern to pass an action closure to a view while minimizing impact, given that closures are hard to compare? Is there a more performant alternative?</h3><blockquote><p>Try to capture as little as possible in closures&#8212;for example, by not relying on implicit captures (which usually capture self and therefore depend on the whole view value) and instead capturing only the properties of the view value that you actually need in the closure.</p></blockquote><p></p><h3>How should I think about a skipped update? Are we saying that frames were skipped?</h3><blockquote><p>No, it means that the view&#8217;s value (that is, all stored properties of a view) was equal to the previous view value and therefore the view&#8217;s body wasn&#8217;t evaluated.</p></blockquote><p></p><h3>How does SwiftUI Canvas performance scale under visionOS multi-window contexts?</h3><blockquote><p>Canvas&#8217;s performance scaling shouldn&#8217;t be affected by using it in a visionOS multi-window context!</p></blockquote><p></p><h3>How do you recommend keeping an Observable model object in sync with a backing store, like a database? Should I use a private backing variable with a <code>didSet</code> to propagate bindings to the database, or is it better to write a custom observable object without the macro?</h3><blockquote><p>Should you choose to do this yourself, I would strongly encourage you to aggregate changes together at a greater scale than just a property change before propagating them to a database. In general, reacting synchronously to individual property changes one at a time is not good for performance.</p></blockquote><p></p><h3>If injecting an Observable view model into a view, should it be stored as @State?</h3><blockquote><p>You don&#8217;t need to store it as <code>@State</code> and it can just be stored in a <code>let</code> or <code>var</code>.</p></blockquote><p></p><h3>Would using a Timer to update my SwiftUI view be costly in terms of performance? For example, if I want to show the current time in hours, minutes, and seconds, but also have other views that depend on how long the timer has been running?</h3><blockquote><p>Yeah, this is fine! If you don&#8217;t need other events to happen in sync with the timer updating, we&#8217;d recommend using a date-relative text, but if you need multiple UI elements to be in sync with a timer, there&#8217;s nothing wrong with doing that. As host has emphasized though, be sure that you&#8217;re only causing updates for views which actually need to change with the timer!</p></blockquote><p></p><h3>When does it make sense (if ever) to use <code>@Binding</code> in a child view to improve performance versus using let? Assume the child view does not update the value of the property passed in by the parent.</h3><blockquote><p>You should prefer using a <code>let</code> if you don&#8217;t need to write back to the binding. In most cases reading a binding is equivalent to just passing the value directly, but in certain situations (such as if the binding is not generated directly from a <code>@State</code>), bindings can add additional overhead.</p></blockquote><p></p><h3>By default, do ScrollViews have transparent backgrounds, even though they might appear white or black depending on display mode?</h3><blockquote><p>Thanks for the great question! Yes, by default SwiftUI ScrollViews have a transparent background. Certain other scrollable views may have additional backgrounds&#8212;for example, List.</p></blockquote><p></p><h3>Why is &#8220;View Debugging &#8594; Rendering&#8221; disabled in Xcode? Does it only work on an actual device and not in a simulator?</h3><blockquote><p>I recommend filing a feedback to request an enhancement to this functionality to provide support for Simulator Run Destinations.</p></blockquote><p></p><h3>Is there a way to export text-based logs from Instruments and the SwiftUI Template for the various drill-downs (cause &amp; effect) that I see in the UI? I want this for feeding into AI chat sessions. I&#8217;ve had some success using Copy / Copy All with AI, but I&#8217;d like a more robust workflow.</h3><blockquote><p>In addition to copying the data out of the detail views in the Instruments window, you can go to <strong>View &#8594; Show Recorded Data</strong> and find the tables starting with &#8220;swiftui-&#8221; to access the raw recorded data. And finally, you can use the xctrace export command on a recorded trace file to export data in an XML format. If that doesn&#8217;t fit your needs, please use Feedback Assistant and explain what you&#8217;re looking for so we can take a look.</p></blockquote><p></p><h3>Is there any guidance for understanding battery usage in more detail than the audio/networking/processing/display/other breakdown in Xcode Organizer? For example, how can I determine what specific networking behavior is involved?</h3><blockquote><p>Thanks for the question! If you&#8217;re looking to understand field data, I&#8217;d recommend using <strong>MetricKit</strong>: <code>MXNetworkTransferMetric</code> for networking and respective other metrics for CPU, Display, GPU, Location, and others. For profiling at-desk, you can use Xcode gauges or Instruments templates. New in Instruments 26, Power Profiler can show you a subsystem-level breakdown of your application&#8217;s power usage. Please tune in for the currently running presentation that describes this tool in more detail or watch the WWDC25 session <strong>&#8220;<a href="https://developer.apple.com/videos/play/wwdc2025/226/">Profile and optimize power usage in your app.</a>&#8221;</strong></p></blockquote><p></p><h3>Are there any special techniques for improving the performance of extensions, such as system extensions on macOS?</h3><blockquote><p>The best tool for profiling system extensions is Instruments. Tools like Time Profiler can help you understand where time is being spent. To let Instruments attach to your extensions, make sure the debugger can attach to it. Then, configure Instruments to target your local device and use the <strong>Attach</strong> option in the target chooser. Some signing tips and tricks can be found in <a href="https://developer.apple.com/documentation/DriverKit/debugging-and-testing-system-extensions?language=objc">Apple&#8217;s documentation:</a>. For a primer on CPU profiling, watch <strong>&#8220;<a href="https://developer.apple.com/videos/play/wwdc2025/308/">Optimize CPU Performance with Instruments</a>&#8221;</strong>.</p></blockquote><p></p><h3>In our app, a Combine Published object updates the UI. We can use either <code>receive(on: RunLoop.main)</code> or <code>receive(on: DispatchQueue.main)</code>, and both seem to work. Is there a recommended choice between the two?</h3><blockquote><p>Both options will schedule work to be completed on the main thread and allow you to update your UI. The decision depends on the exact details. Using <code>DispatchQueue.main</code> will result in your work executing on the main thread as soon as possible. Using <code>RunLoop.main</code> will schedule work onto the <code>RunLoop</code> and can result in delays to your UI updates. Consider a scenario where you are scrolling&#8212;updating your UI frequently while scrolling can degrade performance. In this case, scheduling onto the RunLoop could result in smoother scrolling. However, if you need the UI to update as quickly as possible, scheduling onto <code>DispatchQueue.main</code> is the best choice.</p></blockquote><div><hr></div><h2>Liquid Glass &#128167;</h2><h3>Should we embed ScrollViews or Lists in GlassEffectContainers if the items use Liquid Glass?</h3><blockquote><p>GlassEffectContainer should be applied to conceptually grouped UI elements, as the paths for the glass of each element can blend together. The contents of a scroll view or list are almost always not all part of the same conceptual group of elements, so GlassEffectContainer shouldn&#8217;t wrap your list or scroll view.</p></blockquote><p></p><h3>Are there additional considerations for improving performance on visionOS? Will you share those today?</h3><blockquote><p>Today we will not be presenting visionOS-specific performance considerations. The optimizations shared by the presenter for Liquid Glass are applicable across all platforms including visionOS</p></blockquote><p></p><h3>Would it be beneficial to wrap toolbar buttons in a GlassEffectContainer, or is the toolbar optimized by default?</h3><blockquote><p>Controls in a SwiftUI toolbar will be correctly handled for you behind the scenes! If you&#8217;re placing the controls yourself using other layout primitives, or something like safeAreaBar though, that&#8217;s when adding a GlassEffectContainer becomes important!</p></blockquote><p></p><h3>On Macs, do third-party displays without HDR reduce the rendering effects applied to Liquid Glass?</h3><blockquote><p>Thanks for the great question! Using a non-HDR display would not change the presence of effects applied to Liquid Glass. However, certain effects would be in standard dynamic range instead of high dynamic range.</p></blockquote><div><hr></div><h2>Instruments &#128736;&#65039;</h2><h3>Are Instruments limited only to physical devices? I&#8217;m a beginner with Instruments.</h3><blockquote><p>It depends! Some tools within Instruments support Simulator devices, while others require physical devices. That said, to get a representative metric of what your users will experience, we recommend profiling on a physical device. Ideally, you should always test on the oldest devices supported by your deployment target to understand lower-bound resource constraints.</p></blockquote><p></p><h3>What&#8217;s the difference between a &#8220;hitch&#8221; and a &#8220;hang&#8221; as it relates to the Instruments tool?</h3><blockquote><p>A hang is a noticeable delay in a discrete user interaction, and it&#8217;s almost always the result of long-running work on the main thread. Long-running work on the main thread can also cause hitches, but for hitches, the threshold is lower. Discrete interaction delays only start becoming noticeable as hangs when the main thread is unresponsive for about 50 ms to 100 ms or longer. However, a delay as small as the length of a single refresh interval &#8212; generally between 8 ms and 16 ms, depending on the refresh rate of the display &#8212; can cause a hitch. Delays in the render server can also cause a hitch, but usually aren&#8217;t long enough to cause a hang.</p></blockquote><div><hr></div><h2>Foundation Model Framework &#129470;</h2><h3>Can the Foundation Model understand different languages? For example, if my data and descriptions are in English, but I want the generated language to be Danish?</h3><blockquote><p>Foundation Models on-device system model is multilingual, supporting any language that Apple Intelligence supports. Your prompt and target language can differ. For learning more on how to handle localization, please check out <a href="https://developer.apple.com/documentation/foundationmodels/support-languages-and-locales-with-foundation-models">&#8220;Support languages and locales with Foundation Models&#8221;</a></p></blockquote><p></p><h3>Can several apps use the on-device model concurrently, or is access allowed only by the foreground app? Also, is it correct to assume that a single shared copy of the model&#8217;s weights exists in memory, rather than one copy per process?</h3><blockquote><p>Weights will be shared across several processes. Multiple apps can use Foundation Models simultaneously, but requests can get serialized due to resource constraints, so response time can differ depending on the number of applications.</p></blockquote><p></p><h3>Is there a way to communicate with models via a binary protocol?</h3><blockquote><p>This isn&#8217;t supported today, but if that would be a useful feature for your use case, please capture that in Feedback Assistant!</p></blockquote><p></p><h3>Which is better for performance and response quality: using longer, more precise field names, or shorter names with longer <code>@Guide </code>descriptions?</h3><blockquote><p>The model will see all of the information you provide it, so I would encourage you to think of it in the same way you would if designing for a human to read. Think of the name like a variable name, and the guide like a doc comment. Would a human be more or less confused if you added more detail to the variable name? How about if that detail was in the doc comment instead?</p></blockquote><p></p><h3>How can you improve perceived performance with Foundation Models when you need JSON output responses? The demos showed plain-text responses, not structured output. Can a JSON response be streamed?</h3><blockquote><p>With structured textual data like JSON, you likely won&#8217;t get anything that parses correctly until the model is done streaming. If you just need structured data, you could instead try using a Generable type which contains all the information you need. Generable does support streaming and guarantees correctness at all phases of generation.</p></blockquote><p></p><h3>Does the increase in latency scale linearly with the amount of information in the context window for Foundation Models?</h3><blockquote><p>Yes! Latency does scale roughly linearly with context window size. Remember, though, that you can mitigate some of this latency by keeping the prefix of your prompt consistent so that you get the benefits of prefix caching.</p></blockquote><div><hr></div><h2>Snap</h2><h3>What format did Snap export from their tool so that it could be ingested by Instruments? Where is this format documented?</h3><blockquote><p>It looks like Snap used a custom tool to visualize their trace files, and used signposts to add custom intervals to Instruments traces recorded at desk.</p></blockquote><div><hr></div><h1>Acknowledgments<strong> &#127942;</strong></h1><p>A huge thank-you to everyone who joined in and shared thoughtful, insightful, and engaging questions throughout the session &#8212; your curiosity and input made the discussion truly rich and collaborative.</p><p><strong>Special thanks to:</strong></p><p>Adam Hill, Brett Best, Giorgio Latour, Shakur Bost, Mustafa Khalil, Danny Khan, Jerald Abille, Jonathan Ballerano, Greg Sapienza, Mel Hsu, Kamil Chmielewski, Nicola, Mihaela Mihaljevic, Alexander Steiner, Antonios Keremidis, Brendan Duddridge, Alexander Degtyarev, Sheba Mayer, Alexander Bichurin, Igor Ryzhkov, Tanel Treuberg, Greg Cooksey, Rose Silicon, Sonia Ziv, and Faiq.</p><p>Finally, a heartfelt thank-you to the <strong>Apple team and moderators</strong> for leading session, sharing expert guidance, and offering such clear explanations of the apps optimization techniques. Your contributions made this session an outstanding learning experience for everyone involved.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h1>One more thing&#8230;</h1><p>Ever tried to explain &#8220;Yak Shaving,&#8221; &#8220;Spaghetti Code,&#8221; or &#8220;Imposter Syndrome&#8221;? Now you don&#8217;t have to &#8212; just <em>send a sticker</em>.</p><p><strong><a href="https://apps.apple.com/us/app/tectalk/id6745105035">TecTalk</a></strong> turns everyday developer slang into fun, relatable stickers for your chats. Whether you&#8217;re venting about bugs or celebrating a successful deploy, there&#8217;s a sticker for every tech mood.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!G70v!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!G70v!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png 424w, https://substackcdn.com/image/fetch/$s_!G70v!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png 848w, https://substackcdn.com/image/fetch/$s_!G70v!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png 1272w, https://substackcdn.com/image/fetch/$s_!G70v!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!G70v!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png" width="428" height="428" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1456,&quot;width&quot;:1456,&quot;resizeWidth&quot;:428,&quot;bytes&quot;:1355045,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/177634693?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!G70v!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png 424w, https://substackcdn.com/image/fetch/$s_!G70v!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png 848w, https://substackcdn.com/image/fetch/$s_!G70v!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png 1272w, https://substackcdn.com/image/fetch/$s_!G70v!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F839ce1d4-6de1-416c-a686-c13cbcf52302_4320x4320.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Created by me &#129489;&#8205;&#128187; &#8212; made <em>by a dev, for devs</em> &#8212; and <a href="https://apps.apple.com/us/app/tectalk/id6745105035">available now</a> at a <strong>very affordable price</strong>.</p><p>Express your inner techie. Stop typing. <a href="https://apps.apple.com/us/app/tectalk/id6745105035">Start sticking</a>.</p>]]></content:encoded></item><item><title><![CDATA[Thread-Safe Classes: GCD vs Actors]]></title><description><![CDATA[Old problem - modern solution]]></description><link>https://antongubarenko.substack.com/p/thread-safe-classes-gcd-vs-actors</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/thread-safe-classes-gcd-vs-actors</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Tue, 28 Oct 2025 13:45:11 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/db72b204-f593-41dc-9564-ef5b39329da9_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Updated 30.10.2025: UnfairLock info added<br></em><br>This time, I want to focus more on a pure coding task &#8212; and what can be more intriguing than multithreading, data races, and concurrency? Moreover, these are common and catchy topics in tech interviews. Such themes are always an inspiration for me because they&#8217;re educational, useful, and often highlight areas you&#8217;d never expect to be problematic.</p><p>Our question is:</p><blockquote><p>You have a pretty plain class that stores cached values in a dictionary. Add whatever you think can bring this class to thread-safe version and, if possible, make it performant.</p></blockquote><div><hr></div><h2>Non-Safe Class</h2><p>Let&#8217;s start with the simplest form &#8212; a shared class with a dictionary:</p><pre><code>final class Cache {
    private var cache = [String: Any]()

    func get(_ key: String) -&gt; Any? {
        cache[key]
    }

    func set(_ key: String, value: Any) {
        cache[key] = value
    }
}</code></pre><p>This class <em>looks</em> fine, but it&#8217;s <strong>not thread-safe</strong>.</p><p>If two threads call set() and get() simultaneously, the dictionary <code>([String: Any])</code> may be mutated while being read &#8212; leading to <strong>data races</strong>, <strong>crashes</strong>, or <strong>corrupted state</strong>.</p><h3>&#10060; Race condition example</h3><ul><li><p>Thread A is writing <code>cache[&#8220;id&#8221;] = value</code></p></li><li><p>Thread B is reading <code>cache[&#8221;id&#8221;]</code></p></li><li><p>Both access the same memory region without synchronization &#8594; undefined behavior.</p></li></ul><div><hr></div><h2>Thread-Safe Cache (Concurrent Queue, No Barrier)</h2><p>To fix this, we can protect access to cache with a <strong>concurrent queue</strong> and <strong>synchronous reads</strong>:</p><pre><code>final class ThreadSafeCache {
    private var cache = [String: Any]()
    private let queue = DispatchQueue(label: &#8220;cache&#8221;, attributes: .concurrent)

    func get(_ key: String) -&gt; Any? {
        queue.sync {
            cache[key]
        }
    }

    func set(_ key: String, value: Any) {
        queue.async {
            self.cache[key] = value
        }
    }
}</code></pre><h3>&#9989; What&#8217;s fixed?</h3><ul><li><p>All access happens through the queue, so no direct concurrent mutation of the dictionary</p></li><li><p>Reads (sync) </p></li><li><p>Writes (async) &#8212; though not yet <strong>serialized</strong></p></li></ul><h3>&#9888;&#65039; What&#8217;s not ideal?</h3><p>Without .barrier, <strong>reads and writes may overlap</strong>.</p><p>So while we&#8217;re protected from crashes, we may still see inconsistent states (e.g., read an old value while a write is halfway done).</p><h3>&#9881;&#65039; Could set be .sync?</h3><p>Technically, <strong>yes</strong> &#8212; you could write:</p><pre><code>func set(_ key: String, value: Any) {
    queue.sync {
        self.cache[key] = value
    }
}</code></pre><p>and it would <strong>work correctly</strong> in terms of thread safety:</p><ul><li><p>sync ensures that the write happens before returning</p></li><li><p>cache won&#8217;t be accessed concurrently by another sync or async call on the same queue</p></li></ul><p>BUT &#8212; the real difference is in behavior and risk.</p><p>Let&#8217;s recap a little about what&#8217;s the main difference between <code>sync</code> and <code>async</code>. The difference between them lies in how they handle execution and blocking. A <code>sync</code> call runs synchronously, meaning the current thread waits until the operation is complete before continuing. It is blocking, and no other code after the call runs until the block finishes. This makes it predictable but also potentially dangerous if used incorrectly, for example, when calling sync on the same queue that is already executing work, which can cause a deadlock.</p><p>An <code>async</code> call runs asynchronously, meaning the operation is scheduled to run later, and the current thread continues immediately without waiting. It is non-blocking, allowing other work to proceed while the queued task executes in the background. This improves responsiveness and concurrency but requires additional coordination if you need to know when the task completes.</p><p>In short, sync waits for the task to finish, while async schedules the task and returns immediately. Let&#8217;s dive closely!</p><h3>&#128721; Why .sync on concurrent queue is risky?</h3><p>Using .sync on a <strong>concurrent queue</strong> is fine from another thread, but <strong>dangerous if called from the same queue</strong>.</p><p>Example:</p><pre><code>queue.sync {
    set(&#8221;x&#8221;, value: 1) // calls queue.sync again inside!
}</code></pre><p>This would cause a <strong>deadlock</strong>, because .sync waits for the block to finish &#8212; but that block can&#8217;t start until the first one finishes (it&#8217;s the same queue).</p><p>&#128165; So, if there&#8217;s <em>any</em> chance your set could be called from within another block on queue, you&#8217;ll hang your program.</p><h3>&#128678; Why .async is the safer pattern</h3><p>Using .async avoids this risk:</p><ul><li><p>It never blocks the caller</p></li><li><p>It simply schedules the write on the queue</p></li><li><p>It guarantees safe execution order without deadlock potential</p></li></ul><p>That&#8217;s why <code>.async</code> is the <strong>recommended GCD pattern</strong> for writes &#8212; especially when using .concurrent queues or when you don&#8217;t control all call sites.</p><div><hr></div><h2>Concurrent Reads, Serialized Writes (Barrier)</h2><p>The best-practice pattern is called</p><blockquote><p><strong>Concurrent Reads, Serialized Writes</strong></p></blockquote><p>We use .barrier to ensure that writes execute <em>exclusively</em>, blocking concurrent reads and other writes while updating shared state.</p><pre><code>final class ThreadSafeCache {
    private var cache = [String: Any]()
    private let queue = DispatchQueue(label: &#8220;cache&#8221;, attributes: .concurrent)

    func get(_ key: String) -&gt; Any? {
        queue.sync {
            cache[key]
        }
    }

    func set(_ key: String, value: Any) {
        queue.async(flags: .barrier) {
            self.cache[key] = value
        }
    }
}

//or Migrated Swift Concurrency version

final class ThreadSafeCache: @unchecked Sendable {
    private var cache: [String: Sendable] = [:]
    private let queue = DispatchQueue(label: &#8220;cache&#8221;, attributes: .concurrent)
    
    func get(_ key: String) -&gt; Sendable? {
        queue.sync {
            cache[key]
        }
    }
    
    func set(_ key: String, value: Sendable) {
        queue.async(flags: .barrier) {
            self.cache[key] = value
        }
    }
}</code></pre><h3>&#9989; What&#8217;s improved?</h3><ul><li><p><strong>Reads</strong>: Can run concurrently.</p></li><li><p><strong>Writes</strong>: Wait until all reads complete, then execute exclusively.</p></li><li><p><strong>No races, no inconsistent reads.</strong></p></li></ul><blockquote><p>Barriers only work with <code>concurrent</code> Queues. On serial queue it acts like async call.</p></blockquote><div><hr></div><h2>OSAllocatedUnfairLock</h2><p>Introduced in <strong>iOS 16 / macOS 13</strong>, OSAllocatedUnfairLock is a <strong>modern Swift wrapper</strong> around os_unfair_lock with proper memory management and Sendable safety.</p><blockquote><p><strong>Apple Recommendation:</strong> If you&#8217;ve existing Swift code that uses <code>os_unfair_lock</code>, change it to use <code>OSAllocatedUnfairLock</code> to ensure correct locking behavior.</p></blockquote><p>It lives in the <strong>os module</strong>, and <strong>acts not like value type</strong> despite being it (struct).</p><p>Example:</p><pre><code>import os

final class ModernUnfairLockCache {
    private var cache = [String: Any]()
    private let lock = OSAllocatedUnfairLock()

    func get(_ key: String) -&gt; Any? {
        lock.withLock {
            cache[key]
        }
    }

    func set(_ key: String, value: Any) {
        lock.withLock {
            cache[key] = value
        }
    }
}</code></pre><p>It has few remarkable advantages (not even few): OSAllocatedUnfairLock provides a Swift-native API that is both type-safe and memory-managed, so you don&#8217;t need to deal with raw pointers or manual initialization. It includes a convenient <br><code>withLock { ... }</code> method that automatically locks and unlocks around a critical section, removing the risk of forgetting to release the lock. The lock is ARC-safe, with its lifetime managed automatically by Swift, so no manual memory management is required. It is thread-safe across tasks and fully <code>Sendable</code>-conforming, making it safe to use in Swift Concurrency environments. In terms of performance, it uses the same underlying kernel primitive as <code>os_unfair_lock</code>, providing identical speed and efficiency. Unlike its C-based predecessor, it requires no C interoperability &#8212; the API is fully Swift-native with clean, expressive syntax. Finally, it is safer to use, preventing undefined behavior that could occur when moving or copying the lock in memory.</p><p>Oh I wish Substack had a Tables and this is a comparison table of Locks we might use in modern Swift:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!pGlu!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb9f1723-9684-477c-b265-3bddb960045f_1830x400.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!pGlu!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb9f1723-9684-477c-b265-3bddb960045f_1830x400.png 424w, https://substackcdn.com/image/fetch/$s_!pGlu!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb9f1723-9684-477c-b265-3bddb960045f_1830x400.png 848w, https://substackcdn.com/image/fetch/$s_!pGlu!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb9f1723-9684-477c-b265-3bddb960045f_1830x400.png 1272w, https://substackcdn.com/image/fetch/$s_!pGlu!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb9f1723-9684-477c-b265-3bddb960045f_1830x400.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!pGlu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb9f1723-9684-477c-b265-3bddb960045f_1830x400.png" width="1456" height="318" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fb9f1723-9684-477c-b265-3bddb960045f_1830x400.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:318,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:71917,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/177348033?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb9f1723-9684-477c-b265-3bddb960045f_1830x400.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!pGlu!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb9f1723-9684-477c-b265-3bddb960045f_1830x400.png 424w, https://substackcdn.com/image/fetch/$s_!pGlu!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb9f1723-9684-477c-b265-3bddb960045f_1830x400.png 848w, https://substackcdn.com/image/fetch/$s_!pGlu!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb9f1723-9684-477c-b265-3bddb960045f_1830x400.png 1272w, https://substackcdn.com/image/fetch/$s_!pGlu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb9f1723-9684-477c-b265-3bddb960045f_1830x400.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><div><hr></div><h2>Swift Actor Version (Full Concurrency Safety)</h2><p>With Swift Concurrency, we can eliminate queues and barriers entirely. Actors provide <strong>isolation</strong>: only one task can access actor-isolated state at a time.</p><pre><code>actor ThreadSafeCache {
    private var cache = [String: Sendable]()

    func get(_ key: String) -&gt; Sendable? {
        cache[key]
    }

    func set(_ key: String, value: Sendable) {
        cache[key] = value
    }
}</code></pre><h3>&#9989; What actors guarantee</h3><ul><li><p>cache is <strong>isolated</strong> &#8212; no two tasks can access it simultaneously.</p></li><li><p>All calls are <strong>serialized</strong> by Swift&#8217;s concurrency runtime</p></li><li><p>You can safely call from multiple async contexts:</p></li></ul><pre><code>let cache = SafeCache()

Task {
    await cache.set(&#8221;id&#8221;, value: 42)
}

Task {
    print(await cache.get(&#8221;id&#8221;) ?? &#8220;nil&#8221;)
}</code></pre><p>No explicit locks, queues, or barriers &#8212; the compiler and runtime handle synchronization.</p><h2>&#128300; Under the Hood: How Actors Work</h2><p>Actors internally manage a <strong>queue of pending tasks (jobs)</strong> and an <strong>isActive flag</strong>. When a task calls an actor method, Swift enqueues it to the actor&#8217;s executor. When idle, the actor becomes active, runs one job at a time, and processes its queue sequentially.<br>This simplified diagram shows how actor takes a task from inner queue to execute. And all of them are serial.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!eFGz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70764aed-6954-4ef2-9204-57c9c81db974_1024x1536.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!eFGz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70764aed-6954-4ef2-9204-57c9c81db974_1024x1536.heic 424w, https://substackcdn.com/image/fetch/$s_!eFGz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70764aed-6954-4ef2-9204-57c9c81db974_1024x1536.heic 848w, https://substackcdn.com/image/fetch/$s_!eFGz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70764aed-6954-4ef2-9204-57c9c81db974_1024x1536.heic 1272w, https://substackcdn.com/image/fetch/$s_!eFGz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70764aed-6954-4ef2-9204-57c9c81db974_1024x1536.heic 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!eFGz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70764aed-6954-4ef2-9204-57c9c81db974_1024x1536.heic" width="436" height="654" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/70764aed-6954-4ef2-9204-57c9c81db974_1024x1536.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1536,&quot;width&quot;:1024,&quot;resizeWidth&quot;:436,&quot;bytes&quot;:59806,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/177348033?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70764aed-6954-4ef2-9204-57c9c81db974_1024x1536.heic&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!eFGz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70764aed-6954-4ef2-9204-57c9c81db974_1024x1536.heic 424w, https://substackcdn.com/image/fetch/$s_!eFGz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70764aed-6954-4ef2-9204-57c9c81db974_1024x1536.heic 848w, https://substackcdn.com/image/fetch/$s_!eFGz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70764aed-6954-4ef2-9204-57c9c81db974_1024x1536.heic 1272w, https://substackcdn.com/image/fetch/$s_!eFGz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70764aed-6954-4ef2-9204-57c9c81db974_1024x1536.heic 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Actor inner serial queue execution</figcaption></figure></div><h2>Summary</h2><p>Below you can find a small table comparing all the described solutions by their main points. If you want performance and safety, use <code>.barrier</code>. For modern approaches and full compatibility with Swift 6.2, <strong>actors</strong> are the way to go.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!QzMT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe80e48f3-21f6-4c7f-8371-49d2b0a9dff0_1806x402.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!QzMT!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe80e48f3-21f6-4c7f-8371-49d2b0a9dff0_1806x402.heic 424w, https://substackcdn.com/image/fetch/$s_!QzMT!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe80e48f3-21f6-4c7f-8371-49d2b0a9dff0_1806x402.heic 848w, https://substackcdn.com/image/fetch/$s_!QzMT!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe80e48f3-21f6-4c7f-8371-49d2b0a9dff0_1806x402.heic 1272w, https://substackcdn.com/image/fetch/$s_!QzMT!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe80e48f3-21f6-4c7f-8371-49d2b0a9dff0_1806x402.heic 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!QzMT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe80e48f3-21f6-4c7f-8371-49d2b0a9dff0_1806x402.heic" width="728" height="162" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e80e48f3-21f6-4c7f-8371-49d2b0a9dff0_1806x402.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:324,&quot;width&quot;:1456,&quot;resizeWidth&quot;:728,&quot;bytes&quot;:49708,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/177348033?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe80e48f3-21f6-4c7f-8371-49d2b0a9dff0_1806x402.heic&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!QzMT!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe80e48f3-21f6-4c7f-8371-49d2b0a9dff0_1806x402.heic 424w, https://substackcdn.com/image/fetch/$s_!QzMT!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe80e48f3-21f6-4c7f-8371-49d2b0a9dff0_1806x402.heic 848w, https://substackcdn.com/image/fetch/$s_!QzMT!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe80e48f3-21f6-4c7f-8371-49d2b0a9dff0_1806x402.heic 1272w, https://substackcdn.com/image/fetch/$s_!QzMT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe80e48f3-21f6-4c7f-8371-49d2b0a9dff0_1806x402.heic 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>Apple Documentation</h2><ul><li><p><strong>DispatchQueue</strong> &#8212; Official API reference</p><p><a href="https://developer.apple.com/documentation/dispatch/dispatchqueue">https://developer.apple.com/documentation/dispatch/dispatchqueue</a></p></li><li><p><strong>Dispatch Barrier</strong> &#8212; Explanation of <code>.barrier </code>semantics (works only with concurrent queues)</p><p><a href="https://developer.apple.com/documentation/dispatch/dispatch-barrier">https://developer.apple.com/documentation/dispatch/dispatch-barrier</a></p></li><li><p><strong>dispatch_barrier_async</strong> &#8212; Function-level reference with behavioral details</p><p><a href="https://developer.apple.com/documentation/dispatch/1452797-dispatch_barrier_async">https://developer.apple.com/documentation/dispatch/1452797-dispatch_barrier_async</a></p></li><li><p><strong>DispatchQueue.Attributes</strong> &#8212; Includes concurrent and notes on respecting barriers</p><p><a href="https://developer.apple.com/documentation/dispatch/dispatchqueue/attributes">https://developer.apple.com/documentation/dispatch/dispatchqueue/attributes</a></p></li><li><p><strong>Swift Language Guide: Concurrency</strong> &#8212; Covers actor, async/await, and structured concurrency</p><p><a href="https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency">https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency</a></p></li><li><p><strong>Swift Evolution SE-0306: Actors</strong> &#8212; Official design proposal for Swift Actors</p><p><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0306-actors.md">https://github.com/apple/swift-evolution/blob/main/proposals/0306-actors.md</a></p></li><li><p><strong>WWDC 2017 &#8212; Modernizing Grand Central Dispatch Usage</strong> (Session 706)</p><p><a href="https://developer.apple.com/videos/play/wwdc2017/706/">https://developer.apple.com/videos/play/wwdc2017/706/</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[iOS 26: Foundation Model Framework - Code-Along Q&A]]></title><description><![CDATA[New educational approach from Apple]]></description><link>https://antongubarenko.substack.com/p/ios-26-foundation-model-framework-f6d</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/ios-26-foundation-model-framework-f6d</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Mon, 06 Oct 2025 11:04:23 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/7e65b150-504c-4d95-8799-70dc03ba654a_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As we discovered in the <a href="https://antongubarenko.substack.com/p/ios-26-foundation-model-framework?r=21t43r">previous post</a>, the new code-along session is Apple&#8217;s fresh approach to explaining frameworks like Foundation Models. Alongside the live coding guide, a Q&amp;A session was running, where every question&#8212;whether about the session itself or the framework in general&#8212;was answered. That&#8217;s almost unimaginable generosity from a company whose Developer Forums can sometimes take weeks to respond.</p><p>In this post, as promised, I&#8217;m sharing my questions and those from others, along with answers (and links for some of them). Everything is sorted, split into sections, and grammar-checked. No more delays&#8212;the Q&amp;A awaits!</p><blockquote><p><strong>Disclaimer:</strong> All answers below are valid as of the publication date. Features mentioned as <em>Beta</em> or related to future releases may change or even be reverted in later updates.</p></blockquote><div><hr></div><h2>General Usage &#129470;</h2><h3>What is the maximum number of tokens or characters that can be sent in a prompt when using the on-device foundation model?</h3><blockquote><p>The current on-device model has a 4K context window (4,096 tokens total &#8212; input plus output combined).</p></blockquote><p></p><h3>Does the 4K context limit also apply to returned data? What if it is asked to return 10 (or 100) itineraries? Also, is there a way to get the response back as JSON (for storage or transmission)?</h3><blockquote><p>The context size applies to input and output tokens. For conversion to JSON, a type can be both <code>Generable</code> and <code>Codable</code>. You can then use <code>JSONEncoder</code> to encode the type to JSON data for storage or transmission</p></blockquote><p></p><h3>How can we handle very large inputs that exceed the 4K token context limit?</h3><blockquote><p>You can chunk the input and process it in segments, or summarize context progressively. For large datasets, consider hybrid processing with cloud models.</p></blockquote><p></p><h3>Can we limit token output to a specific number, like a maximum response length?</h3><blockquote><p>Yes, you can set a maximum output token count using <code>GenerationOptions</code>.</p></blockquote><p></p><h3>I use guided generation with an array of strings. The results are shown in a picker intended to be suggestions in my app for next steps. I noticed the first result in the array is often similar to, or even the same as, a previous request. How can I tweak this to get more variety and different outputs?</h3><blockquote><p>You can try adjusting GenerationOptions like SamplingMode and temperature: <a href="https://developer.apple.com/documentation/foundationmodels/generationoptions">https://developer.apple.com/documentation/foundationmodels/generationoptions</a></p></blockquote><h3></h3><h3>Why does the model return the same results even though we didn&#8217;t use any sort of seed in our code?</h3><blockquote><p>The sampling <code>GenerationOption</code> is set to <code>.greedy</code>. This helps the model choose the same tokens every time if given the same input.</p></blockquote><p></p><h3>How much memory does the on-device model take up when loaded?</h3><blockquote><p>The model takes about 1.2GB of RAM once loaded into memory.</p></blockquote><p></p><h3>Is there any API to check how much memory or disk space the model is currently using?</h3><blockquote><p>There isn&#8217;t a direct API, but you can use Instruments or system diagnostics to measure memory and storage usage.</p></blockquote><p></p><h3>I don&#8217;t understand PartiallyGenerated &#8212; why is it needed?</h3><blockquote><p>Good question! This is needed if you want to stream a <code>Generable</code> type, because the model will generate it property-by-property. <code>PartiallyGenerated</code> will turn every property optional, where <code>nil</code> indicates the model hasn&#8217;t generated it yet. That&#8217;s especially useful for e.g. a <code>Bool</code> property, because using <code>false</code> as a default value would be confusing.</p></blockquote><p></p><h3>Can we stream responses from the model as they&#8217;re generated, or do we only receive the final result?</h3><blockquote><p>You can stream responses using Swift&#8217;s async sequences. This allows you to display tokens or sentences as they arrive.</p></blockquote><p></p><h3>Does streaming structured output work the same way as streaming plain text responses?</h3><blockquote><p>Yes, you can stream partial structured output, and it arrives property-by-property, allowing progressive updates in your UI.</p></blockquote><p></p><h3>Does structured output work with nested objects and arrays, or only flat structures?</h3><blockquote><p>Yes, structured output supports nested <code>Generable</code> objects and arrays. Just make sure each type in the structure conforms to <code>Generable</code>.</p></blockquote><p></p><h3>Can we customize the on-device model, such as fine-tuning it with our own data?</h3><blockquote><p>No, the on-device foundation model cannot be fine-tuned. You can, however, guide its responses through system prompts, few-shot examples, and tools.</p></blockquote><p></p><h3>Can we combine both on-device and cloud foundation models within the same app?</h3><blockquote><p>Yes &#8212; you can decide dynamically whether to use the on-device or cloud model depending on the task, network availability, or privacy needs.</p></blockquote><p></p><h3>Is there a way to test different models or versions side by side during development?</h3><blockquote><p>Yes, you can create multiple Session instances with different model identifiers and compare their outputs in the same app.</p></blockquote><p></p><h3>Are there any specific battery consumption considerations when using the on-device model?</h3><blockquote><p>Yes &#8212; running large or frequent generations can increase CPU/GPU use, so consider batching or caching results to save power.</p></blockquote><p></p><h3>Can the model&#8217;s responses be influenced by user preferences or stored data?</h3><blockquote><p>Yes, you can include user-specific context in your prompt or as tool inputs to personalize responses.</p></blockquote><p></p><h3>Can we expect 100% adherence in Structured Output even if the properties are PartiallyGenerated (in which case, as you mentioned before, they are marked as Swift optionals)?</h3><blockquote><p>Yes! <code>Generable</code> uses guided generation to make sure the model always outputs the correct format! And this even works with <code>PartiallyGenerated</code> and optionals. For more information, you can watch the <code>Generable</code> section of our <a href="https://youtu.be/6Wgg7DIY29E?si=pVXw4x5Ao6lpvkgX&amp;t=477">Deep Dive video</a>.</p></blockquote><p></p><h3>Can we use the foundation models framework from a Swift package, or must it be in the main app target?</h3><blockquote><p>You can import and use it in a Swift package as long as the deployment target supports iOS 18 or macOS Sequoia.</p></blockquote><p></p><h3>Can we restrict the model&#8217;s output to a fixed set of strings (like &#8220;yes&#8221; or &#8220;no&#8221;)?</h3><blockquote><p>Yes &#8212; you can use Guided Generation with an enum that conforms to <code>Generable</code>, ensuring the model outputs only valid values.</p></blockquote><p></p><h3>Can the foundation model generate or understand structured data like JSON or XML without using <code>Generable</code>?</h3><blockquote><p>Yes, you can instruct it to output JSON or XML via prompt engineering, but <code>Generable</code> ensures it follows your structure reliably.</p></blockquote><p></p><h3>Are there any sample projects or templates available that demonstrate using tools with the on-device model?</h3><blockquote><p>Yes, you can find example projects in Apple&#8217;s developer documentation and WWDC session materials, particularly &#8220;<a href="https://www.youtube.com/watch?v=XuX66Oljro0">Integrate Foundation Models into Your App.</a>&#8221;</p></blockquote><p></p><h3>Does the model retain context between app launches, or is it reset when the app restarts?</h3><blockquote><p>Context is reset when the app restarts; it&#8217;s tied to the session&#8217;s lifetime, not persisted storage.</p></blockquote><p></p><h3>Can we provide a system prompt to guide behavior, similar to how you can in ChatGPT?</h3><blockquote><p>Yes &#8212; you can provide a system message at the start of your session to define context, tone, or behavior.</p></blockquote><p></p><h3>What happens if the structured output schema doesn&#8217;t match what the model returns?</h3><blockquote><p>If the output doesn&#8217;t match, decoding will fail gracefully &#8212; you&#8217;ll get a partial result or an error depending on your decoding logic.</p></blockquote><p></p><h3>Can we adjust temperature or top-p parameters for generation like we can with cloud models?</h3><blockquote><p>Yes, you can adjust both temperature and sampling mode using <code>GenerationOptions</code>.</p></blockquote><p></p><h3>Are there limitations when running the on-device model in the background or during multitasking?</h3><blockquote><p>Yes, the model is paused or unloaded when your app enters the background to conserve system resources.</p></blockquote><p></p><h3>Can we access token-level probabilities or confidence values from the model&#8217;s output?</h3><blockquote><p>Not at the moment. The on-device foundation models API does not expose per-token probabilities.</p></blockquote><p></p><h3>Does the model automatically handle punctuation and capitalization when generating structured output?</h3><blockquote><p>Yes, the model generates well-formed text and correctly formatted structured outputs by default.</p></blockquote><p></p><h3>Can we interrupt or cancel an in-progress generation request if the user changes their input?</h3><blockquote><p>Yes, you can cancel the ongoing task by calling <code>Task.cancel()</code> on the async operation handling the generation.</p></blockquote><p></p><h3>Can the model process or summarize audio transcripts generated from the Speech framework?</h3><blockquote><p>Yes, as long as you provide the transcript text as input. The foundation model itself does not process audio directly.</p></blockquote><p></p><h3>Does the on-device model support image or multimodal inputs?</h3><blockquote><p>Currently, no. The on-device foundation models support text-only input and output.</p></blockquote><p></p><h3>Can the on-device model generate code snippets or handle technical prompts?</h3><blockquote><p>Yes, but since it&#8217;s smaller than the cloud models, results may be less detailed or accurate for complex technical tasks.</p></blockquote><p></p><h3>How is user privacy handled when using the on-device model?</h3><blockquote><p>All processing occurs entirely on the device. No data is sent to Apple or external servers when using the on-device foundation model.</p></blockquote><div><hr></div><h2>Tools &#128736;&#65039;</h2><h3>Can foundation models be used in a tool (for example, to get an estimated cooking time from cooking instructions)?</h3><blockquote><p>A new API introduced in the current beta of iOS 26.1 also provides access to the transcript of the session from within the Tool, in case it&#8217;s useful to you. But please note that this is still in beta.</p></blockquote><p></p><h3>Why is this tool conforming to <code>@Observable</code>? Is it mandatory for them?</h3><blockquote><p>Not mandatory for all tools. For this specific example, we&#8217;re adding state that we want to observe from our SwiftUI View.</p></blockquote><p></p><h3>Does the context limit also include the description provided by the tool? </h3><blockquote><p>Yes, the description of the tool is automatically included in the prompt when passing the tool to your session.</p></blockquote><p></p><h3>Is a tool&#8217;s response included in the context limit?</h3><blockquote><p>Good question! Yes, it is.</p></blockquote><p></p><h3>Does using tools or structured outputs affect latency compared to plain text generation?</h3><blockquote><p>There&#8217;s a slight overhead, but it&#8217;s minimal &#8212; typically under 10%. The reliability of output formatting usually outweighs the cost.</p></blockquote><p></p><h3>Can we control how the model handles errors, such as when a tool call fails?</h3><blockquote><p>You can catch and handle tool execution errors via Swift&#8217;s error-handling mechanisms in your tool&#8217;s implementation.</p></blockquote><div><hr></div><h2>Optimizations &#128200;</h2><h3>Why would you <em>not</em> want to prewarm the model?</h3><blockquote><p>Prewarming is useful when your app has some idle time waiting for the user to trigger the generation. Prewarming may not help if you are presenting a proactive suggestion that&#8217;s not triggered by the user.</p></blockquote><p></p><h3>What is the estimated efficiency of prewarm? I&#8217;ve noticed in the final app that since I know the expected UI, I can press the button and still get the same delay compared to before I included prewarm.</h3><blockquote><p>prewarm will load the model into memory, ahead of making a request. If the model is already in memory (cached from a recent previous request), prewarm won&#8217;t show a difference. But for the case where the model wasn&#8217;t in memory yet, prewarm can easily save 500ms.</p></blockquote><p></p><h3>Does model loading or prewarming behavior differ between simulator and real devices?</h3><blockquote><p>Yes, the simulator does not fully simulate hardware acceleration, so load and generation times can differ significantly. For accurate profiling, always test on a real device.</p></blockquote><p></p><h3>If we use prewarm, can we unload or &#8220;cool down&#8221; the model later to free memory when not needed?</h3><blockquote><p>Currently, there&#8217;s no explicit unload API. The system manages unloading automatically when memory pressure requires it. You can infer it based on load time &#8212; if a generation suddenly takes longer, the model was likely unloaded.</p></blockquote><p></p><h3>Can we detect if the model is already loaded into memory, to avoid calling prewarm unnecessarily?</h3><blockquote><p>There&#8217;s no public API to check directly. If you call prewarm multiple times, the system will simply ignore redundant calls.</p></blockquote><p></p><h3>Is there a recommended way to cache or reuse a session between multiple user requests, or should we create a new one each time?</h3><blockquote><p>You can reuse a session for multiple related requests (to preserve context). If the requests are independent, creating a new session is fine &#8212; it helps reset the context and avoid unnecessary token buildup.</p></blockquote><p></p><h3>What are the main performance differences between the on-device model and the cloud model?</h3><blockquote><p>The on-device model is faster for short requests and provides privacy benefits, but it has a smaller context window and less raw reasoning power compared to the cloud model.</p></blockquote><p></p><h3>Is there any performance difference between running the same model on iPhone vs. iPad?</h3><blockquote><p>Performance varies slightly depending on device hardware &#8212; newer chips and more RAM provide faster responses and less latency.</p></blockquote><p></p><h3>How large can a structured output object be before hitting performance issues?</h3><blockquote><p>It depends on the complexity and context size. Larger objects mean longer generation time and higher memory use. Generally, staying under a few hundred fields is fine.</p></blockquote><h3></h3><h3>What option/template used for profiling? Is there a recommended approach for debugging model behavior when the output seems inconsistent?</h3><blockquote><p>There is specific template for it. You can also use any template and add the Foundation Models instrument on top. You may find the SwiftUI template or Time Profiler useful, as they will allow you to profile the on-device LLM together with your UI.</p></blockquote><p></p><h3>What&#8217;s the best way to measure real-world latency in an app using foundation models?</h3><blockquote><p>Use the Instruments tool or your own timing metrics around model initialization and generation calls.</p></blockquote><div><hr></div><h2>Localization &#127908;</h2><h3>Does the on-device model support multilingual input and output, or is it optimized for English only?</h3><blockquote><p>The on-device foundation model supports multiple languages, though quality may vary. It performs best with English but can handle several major languages.</p></blockquote><div><hr></div><h2>Concurrency &#127950;&#65039;</h2><h3>Can (or should) we run multiple requests in parallel to a model? What implications does that have on performance? Should we only do one request at a time, or does it work fine if we parallelize requests (and will they even parallelize)?</h3><blockquote><p>For one single session, you can&#8217;t call the respond method while the model is responding. You can create multiple sessions to run multiple requests in parallel. See for more info.</p></blockquote><p></p><h3>Can we chain multiple models or sessions together for multi-step reasoning or planning tasks?</h3><blockquote><p>Yes, you can run multiple sessions sequentially, passing results from one to the next. Each session is independent, so you&#8217;ll need to manage context manually.</p></blockquote><div><hr></div><h2>Apple Review Guidelines &#128373;&#65039;&#8205;&#9794;&#65039;</h2><h3>Are there any Apple guidelines for submitting an App Store app that uses a foundation model?</h3><blockquote><p>In addition to the <a href="https://developer.apple.com/app-store/review/guidelines">App Review Guidelines</a>, apps using the on-device foundation model are subject to the acceptable use <a href="https://developer.apple.com/apple-intelligence/acceptable-use-requirements-for-the-foundation-models-framework">requirements for the Foundation Models framework</a>.</p></blockquote><p></p><h3>What is the consensus on Apple Intelligence-centered apps (i.e., apps that rely on it as a core part of their UI/UX)? It&#8217;s definitely always better to support all scenarios, but I&#8217;m thinking there could be cases where AI is the core part of the app.</h3><blockquote><p>There is a section in the HIG for Generative AI that addresses this. You can find it at <a href="https://developer.apple.com/design/human-interface-guidelines/generative-ai">https://developer.apple.com/design/human-interface-guidelines/generative-ai</a>.</p></blockquote><div><hr></div><h1>Acknowledgments<strong> &#127942; </strong></h1><p>A heartfelt thank-you to everyone who participated and contributed thoughtful, insightful, and engaging questions throughout the session &#8212; your curiosity and input made this discussion both rich and collaborative.</p><p>Special thanks to:</p><p><strong>James Dempsey, David Navalho, Joyal Serrao, Francesco Campanile, Ilian, Bruno Diniz, Bharath, Adam Ure, Pradeep Elankumaran, Bar, Ramin Firoozye, Jordy Witteman, Cristian Dinca, Dan Lee, Isaac Kim, Jin Yan, Alex Paul, Igor Dorogokuplia, Jared Hunter, Akshay, Steve Spigarelli, AJ Ram, Piotr Chojnowski, Lopes, Rajiv Jhoomuck, Jon Judelson, Nikita Korshunov, Chang Chih Hsiang, Chaitanya Kola, John Goering, Rik Visser, Maximilian Blaise Schuchart, Esteban RM, HebertGo, Abraz, Stefan Wille, Dev, Roxana Nagy, Arno Appenzeller, Amay Raj Srivastav, faraz qureshi, Momar, John Anderson, Rohith Yanapu, and Melissa Bain.</strong></p><p>Finally, a sincere thank-you to the <strong>Apple team and moderators</strong> for leading the Code Along, sharing expert guidance, and offering clear explanations of the Foundation Models framework. Your contributions made this session an outstanding learning experience for everyone involved.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[iOS 26: Foundation Model Framework - Code-Along Session]]></title><description><![CDATA[New educational approach from Apple]]></description><link>https://antongubarenko.substack.com/p/ios-26-foundation-model-framework</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/ios-26-foundation-model-framework</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Tue, 30 Sep 2025 08:18:02 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/7cac00dd-3109-4f64-89e3-6d5fb4658fc0_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>My Doubts</h1><p>I&#8217;ve been debating whether to post this, in what format, and whether it might be unfair. However, any study can begin on the Apple Forums, where everyone publicly discusses questions and waits for Apple Engineers to reply. But what if there&#8217;s a shorter, faster way? Let me first explain what happened on <strong>September 25, 2025</strong>.</p><h1>Code-Along Session</h1><p>This year, Apple introduced an <a href="https://developer.apple.com/videos/play/meet-with-apple/205/">online code-along session</a> for developers to join&#8212;a completely new format. In the past there were Labs, on-site events, and 1-on-1 sessions during and after WWDC, but this live-coding webinar stands apart.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>How to Attend</h2><p>As a member of the Apple Developer Program, I received an email invitation.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8cUw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86d780ac-1baa-40f6-9bbe-39b220bfd3c5_1360x928.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8cUw!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86d780ac-1baa-40f6-9bbe-39b220bfd3c5_1360x928.png 424w, https://substackcdn.com/image/fetch/$s_!8cUw!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86d780ac-1baa-40f6-9bbe-39b220bfd3c5_1360x928.png 848w, https://substackcdn.com/image/fetch/$s_!8cUw!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86d780ac-1baa-40f6-9bbe-39b220bfd3c5_1360x928.png 1272w, https://substackcdn.com/image/fetch/$s_!8cUw!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86d780ac-1baa-40f6-9bbe-39b220bfd3c5_1360x928.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8cUw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86d780ac-1baa-40f6-9bbe-39b220bfd3c5_1360x928.png" width="1360" height="928" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/86d780ac-1baa-40f6-9bbe-39b220bfd3c5_1360x928.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:928,&quot;width&quot;:1360,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:190885,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/174599156?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86d780ac-1baa-40f6-9bbe-39b220bfd3c5_1360x928.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!8cUw!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86d780ac-1baa-40f6-9bbe-39b220bfd3c5_1360x928.png 424w, https://substackcdn.com/image/fetch/$s_!8cUw!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86d780ac-1baa-40f6-9bbe-39b220bfd3c5_1360x928.png 848w, https://substackcdn.com/image/fetch/$s_!8cUw!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86d780ac-1baa-40f6-9bbe-39b220bfd3c5_1360x928.png 1272w, https://substackcdn.com/image/fetch/$s_!8cUw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86d780ac-1baa-40f6-9bbe-39b220bfd3c5_1360x928.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Courtesy of Apple Inc.</figcaption></figure></div><p>After signing a short form linked to my Apple Developer account, another email with a calendar event arrived in my inbox. Then it was just a matter of waiting.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mOyl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F394b67e8-9ad6-4d16-a830-f88e3f43de7f_1182x712.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mOyl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F394b67e8-9ad6-4d16-a830-f88e3f43de7f_1182x712.png 424w, https://substackcdn.com/image/fetch/$s_!mOyl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F394b67e8-9ad6-4d16-a830-f88e3f43de7f_1182x712.png 848w, https://substackcdn.com/image/fetch/$s_!mOyl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F394b67e8-9ad6-4d16-a830-f88e3f43de7f_1182x712.png 1272w, https://substackcdn.com/image/fetch/$s_!mOyl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F394b67e8-9ad6-4d16-a830-f88e3f43de7f_1182x712.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mOyl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F394b67e8-9ad6-4d16-a830-f88e3f43de7f_1182x712.png" width="1182" height="712" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/394b67e8-9ad6-4d16-a830-f88e3f43de7f_1182x712.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:712,&quot;width&quot;:1182,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:104044,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/174599156?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F394b67e8-9ad6-4d16-a830-f88e3f43de7f_1182x712.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!mOyl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F394b67e8-9ad6-4d16-a830-f88e3f43de7f_1182x712.png 424w, https://substackcdn.com/image/fetch/$s_!mOyl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F394b67e8-9ad6-4d16-a830-f88e3f43de7f_1182x712.png 848w, https://substackcdn.com/image/fetch/$s_!mOyl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F394b67e8-9ad6-4d16-a830-f88e3f43de7f_1182x712.png 1272w, https://substackcdn.com/image/fetch/$s_!mOyl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F394b67e8-9ad6-4d16-a830-f88e3f43de7f_1182x712.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Courtesy of Apple Inc.</figcaption></figure></div><h1>Day of the Event</h1><p>The link was later updated and, besides the usual info, now included a Resources section in the footer.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!oXKl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c349301-d189-4ef7-a4c4-48c14d68ebee_770x232.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!oXKl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c349301-d189-4ef7-a4c4-48c14d68ebee_770x232.png 424w, https://substackcdn.com/image/fetch/$s_!oXKl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c349301-d189-4ef7-a4c4-48c14d68ebee_770x232.png 848w, https://substackcdn.com/image/fetch/$s_!oXKl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c349301-d189-4ef7-a4c4-48c14d68ebee_770x232.png 1272w, https://substackcdn.com/image/fetch/$s_!oXKl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c349301-d189-4ef7-a4c4-48c14d68ebee_770x232.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!oXKl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c349301-d189-4ef7-a4c4-48c14d68ebee_770x232.png" width="506" height="152.45714285714286" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3c349301-d189-4ef7-a4c4-48c14d68ebee_770x232.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:232,&quot;width&quot;:770,&quot;resizeWidth&quot;:506,&quot;bytes&quot;:34555,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/174599156?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c349301-d189-4ef7-a4c4-48c14d68ebee_770x232.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!oXKl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c349301-d189-4ef7-a4c4-48c14d68ebee_770x232.png 424w, https://substackcdn.com/image/fetch/$s_!oXKl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c349301-d189-4ef7-a4c4-48c14d68ebee_770x232.png 848w, https://substackcdn.com/image/fetch/$s_!oXKl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c349301-d189-4ef7-a4c4-48c14d68ebee_770x232.png 1272w, https://substackcdn.com/image/fetch/$s_!oXKl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c349301-d189-4ef7-a4c4-48c14d68ebee_770x232.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Courtesy of Apple Inc.</figcaption></figure></div><p>Here&#8217;s what the instructions meant: a code-along is where you work on a project and its features <strong>together with an Apple Engineer</strong>. In this session, we learned how to add Apple&#8217;s on-device LLM to an app using the Foundation Models framework&#8212;building generative-AI features in Xcode with text generation, structured output, streaming, and tool calling.</p><p>Following the live demo, we used Playground snippets to test APIs, then applied them to update SwiftUI views and view models. By the end, we had a working sample app and the know-how to bring Foundation Models into our own projects. Isn&#8217;t that great?</p><h1>How It Went</h1><p>After a short intro&#8212;and after confirming that macOS Tahoe and Xcode 26 were set up and the project built correctly&#8212;we followed the host. The Foundation Models framework was explained from basics to performance tweaks:</p><ul><li><p>What is the Framework</p></li><li><p>When it suggested to be used</p></li><li><p>How to make a basic prompt</p></li><li><p>Then how to make a dynamic one</p></li><li><p><code>@Generable</code> macro</p></li><li><p>Tooling</p></li><li><p>Performance suggestions and tips</p></li></ul><p><strong>All of this came with live demos and interaction.</strong> For me, as someone who attended similar events years ago, it was fun and engaging. What&#8217;s better than applying and coding the logic from scratch? I&#8217;d definitely join again for other topics.</p><h1>When Will be Other One</h1><p>The format is still taking shape. Apple gathered feedback right after the code-along and is reviewing interest and topic coverage. In private chats, most of my friends said they&#8217;ll attend next time and are thrilled that such an online format now exists.</p><p>Here&#8217;s one last&#8212;and just as valuable&#8212;detail. During the session a Q&amp;A tool let us ask questions about the stream or the framework in general. The host wasn&#8217;t interrupted because assistant developers were answering questions and sharing links in real time. Imagine Apple Forums with instant replies&#8212;<strong>for two hours straight!</strong></p><p>Some questions were about setup and session steps, but many focused on generative-model limitations, contexts, and localization. Some info had been highlighted at WWDC, some discovered during iOS 26 research and adoption, and many questions were brand-new and fascinating. I found plenty of new insights even during the stream.</p><p>For example, we learned how to set model availability from the Scheme Context and provide user feedback about it&#8212;within the first five minutes! There were several other moments like this that made the event completely worthwhile.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!eYZP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe943d08a-c415-4608-906a-9a915478e651_1152x348.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!eYZP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe943d08a-c415-4608-906a-9a915478e651_1152x348.png 424w, https://substackcdn.com/image/fetch/$s_!eYZP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe943d08a-c415-4608-906a-9a915478e651_1152x348.png 848w, https://substackcdn.com/image/fetch/$s_!eYZP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe943d08a-c415-4608-906a-9a915478e651_1152x348.png 1272w, https://substackcdn.com/image/fetch/$s_!eYZP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe943d08a-c415-4608-906a-9a915478e651_1152x348.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!eYZP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe943d08a-c415-4608-906a-9a915478e651_1152x348.png" width="1152" height="348" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e943d08a-c415-4608-906a-9a915478e651_1152x348.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:348,&quot;width&quot;:1152,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:143264,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/174599156?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe943d08a-c415-4608-906a-9a915478e651_1152x348.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!eYZP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe943d08a-c415-4608-906a-9a915478e651_1152x348.png 424w, https://substackcdn.com/image/fetch/$s_!eYZP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe943d08a-c415-4608-906a-9a915478e651_1152x348.png 848w, https://substackcdn.com/image/fetch/$s_!eYZP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe943d08a-c415-4608-906a-9a915478e651_1152x348.png 1272w, https://substackcdn.com/image/fetch/$s_!eYZP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe943d08a-c415-4608-906a-9a915478e651_1152x348.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" style="height:20px;width:20px" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And that on a first 5 minutes ) There were some other moments like this which totally made me satisfied after attending.</p><h1>What Next</h1><p>After experiencing this event, it&#8217;s no surprise that I recommend it to developers of all backgrounds. It&#8217;s great to see new approaches to engaging us, even though webinars have existed for years.</p><p>Apple is catching up to community expectations: from a growing YouTube channel to extra labs throughout the year&#8212;and now this. The more ways to learn, the better, since some prefer videos, others articles, and others slides. A code-along gives you all of that in one place.</p><p>The Q&amp;A session included so many questions that I&#8217;ll cover it in my next post. That way it won&#8217;t be buried here, and you&#8217;ll already know the context.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>