<?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:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Soumya Mahunt on Medium]]></title>
        <description><![CDATA[Stories by Soumya Mahunt on Medium]]></description>
        <link>https://medium.com/@soumyamahunt?source=rss-41d824adb39d------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*tQyHQ70idYEo_8GeU0FvDg.jpeg</url>
            <title>Stories by Soumya Mahunt on Medium</title>
            <link>https://medium.com/@soumyamahunt?source=rss-41d824adb39d------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Mon, 11 May 2026 20:16:34 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@soumyamahunt/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[What you should know before Migrating from GCD to Swift Concurrency]]></title>
            <link>https://soumyamahunt.medium.com/what-you-should-know-before-migrating-from-gcd-to-swift-concurrency-74d4d9b2c4e1?source=rss-41d824adb39d------2</link>
            <guid isPermaLink="false">https://medium.com/p/74d4d9b2c4e1</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[ios-development]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[apple]]></category>
            <dc:creator><![CDATA[Soumya Mahunt]]></dc:creator>
            <pubDate>Wed, 11 Mar 2026 12:34:57 GMT</pubDate>
            <atom:updated>2026-03-12T04:20:09.203Z</atom:updated>
            <content:encoded><![CDATA[<h4>Swift Concurrency migration nuances that nobody is telling you</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dHeXFo0zEzEmbq50_ETeUA.jpeg" /></figure><p>Swift Concurrency is one of the most transformative additions to Swift since the language launched. It promises safer, more readable asynchronous workflows compared to the callback‑heavy days of Grand Central Dispatch (GCD).</p><p>But when moving existing code from GCD to Swift Concurrency, it’s not <em>just</em> replacing DispatchQueue calls with async/await and actor isolation, resolving all the compiler warnings and errors (<a href="https://www.swift.org/migration/documentation/migrationguide/">like the official migration guide suggests</a>). The migration comes with subtle differences in execution at runtime, missing equivalents, and API pitfalls.</p><p>In this post, I’ll share real-world gotchas from my own migration journey — plus side-by-side code comparisons so you can see exactly how to translate old patterns into modern concurrency.</p><p>We will start with a simple GCD based bank account implementation and see the migration to Swift Concurrency:</p><pre>class BankAccount {<br>    private var balance: Int<br>    let queue = DispatchQueue(label: &quot;com.bank.account&quot;)<br><br>    init(balance: Int) {<br>        self.balance = balance<br>    }<br><br>    func withdraw(amount: Int) {<br>        queue.async {<br>            self.balance -= amount<br>        }<br>    }<br><br>    func deposit(amount: Int) {<br>        queue.async {<br>            self.balance += amount<br>        }<br>    }<br>}</pre><h3>1️⃣ Execution Order Isn’t the Same</h3><p>GCD guarantees FIFO execution order for tasks queued on the same serial queue. Even for concurrent queue, the work items are started in the order of submission. Swift Concurrency schedules tasks onto a cooperative thread pool — the order is not guaranteed (even though your work share the same priority level).</p><p>Following snippet in GCD, always executes in order:</p><pre>let queue = DispatchQueue(label: &quot;serial&quot;)<br>queue.async { print(&quot;First&quot;) }<br>queue.async { print(&quot;Second&quot;) }<br>// Always prints First then Second</pre><p>While the common suggested Swift Concurrency approach can print out of order:</p><pre>Task { print(&quot;First&quot;) }<br>Task { print(&quot;Second&quot;) }<br>// Order is not guaranteed</pre><h4>Why is this important?</h4><p>In common migration guide it is suggested to use continuations to create async versions of methods with completion handler and replace call to the older method with Task wrapped async method.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-62BiVmJO2_Z6gNs9bKRAA.jpeg" /><figcaption>Invalid conversion of async method into completion</figcaption></figure><p>The following conversion for our GCD code is invalid as Tasks created in withdraw and deposit functions can be executed out of order unless being invoked from an actor that guarantees serial execution:</p><pre>actor BankAccount {<br>    var balance: Int<br><br>    init(balance: Int) {<br>        self.balance = balance<br>    }<br><br>    func withdraw(_ amount: Int) {<br>        balance -= amount<br>    }<br><br>    func deposit(_ amount: Int) {<br>        balance += amount<br>    }<br><br>    nonisolated func withdraw(amount: Int) {<br>        Task {<br>            await bankAccount.withdraw(amount)<br>        }<br>    }<br><br>    nonisolated func deposit(amount: Int) {<br>        Task {<br>            await bankAccount.deposit(amount)<br>        }<br>    }<br>}</pre><p>The subtle state difference in the new async method execution to the older one can cause data races and hard to reproduce bugs.</p><h4>What should be done?</h4><p>To maintain execution order call the asynchronous function directly. While this will require adding async to the parent method now, entire method chain should be migrated.</p><p>If you have to use Task use a TaskExecutor or a GlobalActor. Make sure to use DispatchQueue as the executor in these cases to avoid execution order differences.</p><p>Our GCD code equivalent becomes:</p><pre>actor BankAccount {<br>    var balance: Int<br><br>    init(balance: Int) {<br>        self.balance = balance<br>    }<br><br>    func withdraw(_ amount: Int) {<br>        balance -= amount<br>    }<br><br>    func deposit(_ amount: Int) {<br>        balance += amount<br>    }<br><br>    nonisolated func withdraw(amount: Int) {<br>        Task { @MyGlobalActor in<br>            await bankAccount.withdraw(amount)<br>        }<br>    }<br><br>    nonisolated func deposit(amount: Int) {<br>        Task { @MyGlobalActor in<br>            await bankAccount.deposit(amount)<br>        }<br>    }<br>}</pre><blockquote><strong>This isn’t the complete migration, we will discuss the issues with this code in next sections.</strong></blockquote><h3>2️⃣ Actors aren’t DispatchQueues</h3><p>When migrating from GCD to Swift Concurrency, you might be tempted to think of an actor as a drop‑in replacement for a serial DispatchQueue.</p><p>While both ensure mutually exclusive access to the data they protect, actors are not queues — they are part of the structured concurrency model and have different semantics. The key difference being the order of execution by actors compared to DispatchQueues when dealing with task priority. Consider the following example:</p><pre>actor SomeActor {<br>    var counter = 0<br><br>    func log(external: Int) {<br>        if counter != external {<br>            print(&quot;Counter \(counter) with \(external)&quot;)<br>        }<br>        counter += 1<br>    }<br>}<br><br><br>await withTaskGroup { group in<br>    let act = SomeActor()<br>    for index in 0..&lt;1000 {<br>        let task = Task.immediate(priority: (index % 2 == 0) ? .high : .low) {<br>            await act.log(external: index)<br>        }<br>        group.addTask {<br>            await task.value<br>        }<br>    }<br>}<br><br>// Unordered output</pre><h4>Why is this important?</h4><p>While migrating you might be replacing all the shared mutation access with DispatchQueues to actors. But without considering the task priority at these actor call site you might introduce data races.</p><p>The final snippet in our GCD migrated code in the above section still has this issue.</p><h4>What should be done?</h4><p>Fortunately, actors allow to customize their execution with <a href="https://developer.apple.com/documentation/swift/actor/unownedexecutor">custom </a><a href="https://developer.apple.com/documentation/swift/actor/unownedexecutor">SerialExecutor</a>. Using DispatchQueue as underlying executor preserves the existing behaviour while migrating.</p><pre>actor SomeActor {<br>    var counter = 0<br><br>    let queue = DispatchSerialQueue(label: &quot;cusomQueue&quot;)<br>    nonisolated var unownedExecutor: UnownedSerialExecutor { <br>        queue.asUnownedSerialExecutor()<br>    }<br><br>    func log(external: Int) {<br>        if counter != external {<br>            print(&quot;Counter \(counter) with \(external)&quot;)<br>        }<br>        counter += 1<br>    }<br>}</pre><p>The correct migration for out GCD equivalent becomes:</p><pre>actor BankAccount {<br>    var balance: Int<br><br>    let queue = DispatchSerialQueue(label: &quot;cusomQueue&quot;)<br>    nonisolated var unownedExecutor: UnownedSerialExecutor { <br>        queue.asUnownedSerialExecutor()<br>    }<br><br>    init(balance: Int) {<br>        self.balance = balance<br>    }<br><br>    func withdraw(_ amount: Int) {<br>        balance -= amount<br>    }<br><br>    func deposit(_ amount: Int) {<br>        balance += amount<br>    }<br><br>    nonisolated func withdraw(amount: Int) {<br>        Task { @MyGlobalActor in<br>            await bankAccount.withdraw(amount)<br>        }<br>    }<br><br>    nonisolated func deposit(amount: Int) {<br>        Task { @MyGlobalActor in<br>            await bankAccount.deposit(amount)<br>        }<br>    }<br>}</pre><h3>3️⃣ assumeIsolated and Older iOS Versions</h3><p>Before <strong>Swift 6</strong>, dispatch didn’t work well with Actor.assumeIsolated. It can report false negatives — where you are actually on the correct queue but assumeIsolated doesn’t know it. Such cases can result in random crashes.</p><p><a href="https://forums.swift.org/t/actor-assumeisolated-erroneously-crashes-when-using-a-dispatch-queue-as-the-underlying-executor/72434/4">The fix for this requires a new version of the Swift runtime, for older systems we need a hack to emulate the new </a><a href="https://forums.swift.org/t/actor-assumeisolated-erroneously-crashes-when-using-a-dispatch-queue-as-the-underlying-executor/72434/4">assumeIsolated for </a><a href="https://forums.swift.org/t/actor-assumeisolated-erroneously-crashes-when-using-a-dispatch-queue-as-the-underlying-executor/72434/4">Dispatch.</a></p><pre>nonisolated func assumeIsolatedHack&lt;T&gt;(<br>    _ block: (isolated SomeActor) throws -&gt; T<br>) rethrows -&gt; T where T: Sendable {<br>    if #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, *) {<br>        return try self.assumeIsolated(block)<br>    } else {<br>        dispatchPrecondition(condition: .onQueue(queue))<br>        // for nonescaping closures, storing as local variable<br>        // inside function not supported. Use this approach<br>        // to temorarily store them as local variables.<br>        return try withoutActuallyEscaping(block) {<br>            // remove isolation and execute<br>            let isolationStripped = unsafeBitCast($0, to: ((SomeActor) throws -&gt; T).self)<br>            return try isolationStripped(self)<br>        }<br>    }<br>}</pre><h4>Why is this important?</h4><p>This might not affect you if you are targeting <strong>iOS 18</strong> <strong>and above</strong> (or equivalent versions for other Apple platforms) or you don’t use DispatchQueues as executors. But if your codebase qualify for these criteria, you might get <strong>random crashes</strong> during migration, that are hard to debug and explain.</p><h4>What should be done?</h4><p>Use the assumeIsolatedHack to unsafely assume isolation instead of the default assumeIsolated.</p><h3>4️⃣ DispatchGroup Has No Actual Equivalent</h3><p>Swift Concurrency offers TaskGroup and async let for running tasks in parallel, but these API behave fundamentally different than the DispatchGroup usages.</p><p>Consider a simple use-case of DispatchGroup:</p><pre>let group = DispatchGroup()<br>// first work<br>group.enter()<br>firstWork { group.leave() }<br>// second work<br>group.enter()<br>secondWork { group.leave() }<br>// completion<br>group.notify { /* completed */ }</pre><p>In the above snippet both firstWork and secondWork are already started (in order) by the end of the current execution context. But for the Swift Concurrency equivalents:</p><pre>async let result1 = await firstWork() // not started immidiately<br>async let result2 = await secondWork() // not started immidiately<br>// started out of order</pre><pre>await withTaskGroup { group in // not started immidiately<br>    group.addTask { // not started immidiately<br>        await firstWork()<br>    }<br>    group.addTask { // not started immidiately<br>        await secondWork()<br>    }<br>}</pre><p>are not started immediately. While in case of TaskGroup execution order can be customized with a TaskExecutor or a GlobalActor, same is not possible yet for async let.</p><h4>Why is this important?</h4><p>These execution difference might cause subtle state differences when your concurrent tasks are being started causing hard to reproduce data races and bugs.</p><h4>What should be done?</h4><p>If you are targeting <strong>iOS 26 and above </strong>(or equivalent versions for other Apple platforms) the actual equivalent of DispatchGroup would be:</p><pre>let task1 = Task.immediate { await firstWork() } // started immidiately<br>let task2 = Task.immediate { await secondWork() } // started immidiately<br>await [task1.value, task2.value]<br>// or<br>await withTaskGroup { group in<br>    group.addTask {<br>        await task1.value<br>    }<br>    group.addTask {<br>        await task2.value<br>    }<br>}</pre><p>If you are targeting below these versions, consider separating the synchronous parts that perform state access into separate functions and then use TaskGroup and async let to perform concurrent work. i.e. <strong>synchronously build your network requests first and then concurrently execute the network calls.</strong></p><h3>🏁 Final Thoughts</h3><p>Swift Concurrency gives us:</p><p>✅ Better structure <br>✅ Automatic order reasoning via await<br>✅ Cleaner bridging to async code<br>✅ Compile time data race checks</p><p>…but it is <strong>not</strong> full proof. There are:</p><ul><li>Changes in execution semantics</li><li>OS version compatibility considerations</li><li>Gaps where GCD still fits better</li></ul><p>A realistic migration strategy often means running <strong>hybrid code </strong>— adopt Swift Concurrency for new APIs &amp; critical paths, while keeping some GCD constructs where their flexibility still wins. While migrating think on the impact on the execution order and state access order.</p><p><strong><em>Stay subscribed for next post on how to approach migration step by step.</em></strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=74d4d9b2c4e1" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building Deployment Pipelines for Mobile SDKs — Part 3]]></title>
            <link>https://medium.com/life-at-moengage/building-deployment-pipelines-for-mobile-sdks-part-3-3d4da6b915b4?source=rss-41d824adb39d------2</link>
            <guid isPermaLink="false">https://medium.com/p/3d4da6b915b4</guid>
            <category><![CDATA[ci-cd-pipeline]]></category>
            <category><![CDATA[sdk]]></category>
            <category><![CDATA[deployment-pipelines]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[apple]]></category>
            <dc:creator><![CDATA[Soumya Mahunt]]></dc:creator>
            <pubDate>Mon, 02 Feb 2026 04:48:22 GMT</pubDate>
            <atom:updated>2026-02-02T04:48:22.345Z</atom:updated>
            <content:encoded><![CDATA[<h3>Building Deployment Pipelines for Mobile SDKs — Part 3</h3><h4>Deployment Pipeline for Apple SDKs</h4><p>In <a href="https://medium.com/life-at-moengage/building-deployment-pipelines-for-mobile-sdks-part-1-96ad8debebc6"><em>Part 1</em></a><em> and </em><a href="https://medium.com/life-at-moengage/building-deployment-pipelines-for-mobile-sdks-part-2-7e10a0a2c6b1"><em>Part 2</em></a><em> </em>of this series, we touched on the importance of implementing deployment pipelines and the rationale for our selection of tech stack, with a walkthrough of the Android SDK pipeline.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fANwwSr5xmts_a-lVhiNNw.jpeg" /><figcaption>Image created using Gemini</figcaption></figure><p>Now, let’s delve into the significant steps that were involved in building the deployment pipeline for Apple SDKs at MoEngage. We’ll also share the goals we set for ourselves, the challenges we faced in achieving those, and the strategies we adopted to overcome them.</p><h4>Goals for Apple SDK Release Process</h4><ul><li><strong>Wider ecosystem support:</strong> We wanted to support all the popular dependency management tools available for development on Apple platforms.</li><li><strong>Seamless SDK integration:</strong> We wanted SDK version management to be as seamless, performant, and secure as possible for customers’ apps.</li><li><strong>Efficient and Reliable Release Process: </strong>We want the process to be as cost and time-efficient while minimizing failures.</li></ul><h4>Wider ecosystem support</h4><p>As an SDK vendor, we want our customers to be able to use our SDK with their favourite dependency management tools. Currently Apple ecosystem has two major dependency managers: <a href="https://cocoapods.org/">CocoaPods</a> and <a href="https://www.swift.org/documentation/package-manager/">Swift Package Manager</a>. While CocoaPods is a legacy dependency manager in Apple eco-system and <a href="https://blog.cocoapods.org/CocoaPods-Specs-Repo/">is soon going to be sunset</a>, it is still popular in older apps and hybrid platforms like Flutter and React Native.</p><p>Supporting these two dependency managers is challenging because both dependency managers are different in their fundamental philosophy and management process.</p><p><strong>Centralized vs de-centralized:</strong> While CocoaPods acts as a centralized dependency manager, where each dependencies are pods and publish to a <a href="https://github.com/CocoaPods/Specs">central repository</a>, SPM acts as a de-centralized dependency manager, allowing authors to specify version with tags in their own repository.</p><p>Due to de-centralized nature of SPM, the dependency manager has to clone the entire dependency repository to know supported versions. To make this cloning as efficient as possible, we decided to keep the repository size minimal with only essential files. Large files like XCFrameworks are published in the <a href="https://github.com/moengage/apple-sdk/releases/download/9.23.0/MoEngageInApps.xcframework.zip">GitHub releases</a> while being referred by URL in the SPM Package manifest and separate podspec files (this introduced additional challenges, explored further in the next section).</p><p><strong>Package vs Podspec:</strong> CocoaPods allows dependency configuration with podspec files, allowing only one target to be defined per podspec. This differs from SPM, which allows only one Package.swift per repo containing multiple products and targets as dependencies. Since we expose multiple XCFrameworks withthe ability for our customers to pick and choose based on desired functionalities, this introduced additional challenges for us.</p><p>We decided to avoid separate repositories for each XCFramework due to:</p><ul><li>Maintenance overhead and complexity of managing and publishing to multiple repositories</li><li>Each repository will have independent versioning, causing conflicts with version resolution.</li><li>For SPM, integrating each new repository introduces one additional step of specifying the URL and version.</li></ul><p>Supporting both from a single repository requires multiple podspec files, each representing a single XCFramework and a single Package.swift file with each product representing an XCFramework. This setup introduced an additional challenge of managing tagging as CocoPods requires each podspec version to be represented by a separate tag, while SPM only allows one version tag. We decided to use a version tag for SPM (i.e. 9.23.1), while using a prefixed version tag (i.e. module-6.5.0) for CocoaPods.</p><p><strong>Avoiding tooling limitations:</strong> <a href="https://github.com/CocoaPods/CocoaPods/issues/12100">CocoaPods has a bug with the implementation of XCFramework integrations, where multiple XCFrameworks in a single </a><a href="https://github.com/CocoaPods/CocoaPods/issues/12100">podspec will not allow dSYM file being copied to application ipa</a>. This was one of the driving factor of using separate podspecs for each XCFrameworks.</p><p><a href="https://github.com/swiftlang/swift-package-manager/issues/8058">SPM has limitation with regarding to XCFrameworks where targets declaring XCFrameworks can’t depend on other targets</a>. We used the current workaround for this to create a wrapper target with dummy source file to declare dependency.</p><pre>// swift-tools-version:5.5<br>// The swift-tools-version declares the minimum version of Swift required to build this package.<br>// This file generated from post_build script, modify the script instead of this file.<br><br>import PackageDescription<br><br>let package = Package(<br>    name: &quot;iOS-SDK&quot;,<br>    platforms: [.iOS(.v11), .tvOS(.v11)],<br>    products: [], dependencies: [], targets: [],<br>    swiftLanguageVersions: [.v5]<br>)<br><br>struct PackageProduct {<br>    let name: String<br>    let targets: [Target]<br>}<br><br>extension Collection where Element == Target.Dependency {<br>    static var `default`: [Target.Dependency] {<br>        return [<br>            &quot;Analytics&quot;, &quot;Core&quot;, &quot;Messaging&quot;,<br>            &quot;ObjCUtils&quot;, &quot;SDK&quot;, &quot;Security&quot;,<br>        ]<br>    }<br><br>    static func additional(dependency: Target.Dependency) -&gt; [Target.Dependency] {<br>        var dependencies = Self.default<br>        dependencies.append(dependency)<br>        return dependencies<br>    }<br>}<br><br>let products: [PackageProduct] = [<br>    .init(<br>        name: &quot;InApps&quot;,<br>        targets: [<br>            .binaryTarget(name: &quot;InApps&quot;, url: &quot;https://github.com/user/apple-sdk/releases/download/9.23.0/InApps.xcframework.zip&quot;, checksum: &quot;bc73edbb6438af435272c9f699e995d1d30ff4c11a2efbd5cd741d7e614b6b13&quot;),<br>            // wrapper target containing dummy surce file for dependency declaration<br>            .target(name: &quot;InAppSPM&quot;, dependencies: .additional(dependency: &quot;TriggerEvaluator&quot;)),<br>        ]<br>    ),<br>]<br><br>for product in products {<br>    for target in product.targets {<br>        package.targets.append(target)<br>    }<br>    package.products.append(<br>        .library(name: product.name, targets: product.targets.map { $0.name })<br>    )<br>}</pre><p><strong>Maintaining Consistency:</strong> To make the dependencies consistent across dependency managers, we decided to use a single package.json file to maintain common configurations, i.e., XCFramework name, download URL, version, and podspec tag prefix, etc.</p><pre>{<br>  &quot;name&quot;: &quot;iOS-SDK&quot;,<br>  &quot;version&quot;: &quot;9.23.1&quot;,<br>  &quot;tagPrefix&quot;: &quot;all-&quot;,<br>  &quot;postBuild&quot;: &quot;Utilities/post_build.rb&quot;,<br>  &quot;packages&quot;: [<br>    {<br>      &quot;name&quot;: &quot;SDK&quot;,<br>      &quot;version&quot;: &quot;9.20.0&quot;,<br>      &quot;tagPrefix&quot;: &quot;sdk-&quot;,<br>      &quot;hash&quot;: &quot;25994092c82b0ce008eaf08fefe6964338b9652c93e28087a7adbb752f59fe37&quot;,<br>      &quot;url&quot;: &quot;https://github.com/user/iOS-SDK/releases/download/9.21.0/SDK.xcframework.zip&quot;<br>    },<br>    {<br>      &quot;name&quot;: &quot;InApps&quot;,<br>      &quot;version&quot;: &quot;6.05.0&quot;,<br>      &quot;tagPrefix&quot;: &quot;inapp-&quot;,<br>      &quot;hash&quot;: &quot;bc73edbb6438af435272c9f699e995d1d30ff4c11a2efbd5cd741d7e614b6b13&quot;,<br>      &quot;url&quot;: &quot;https://github.com/user/apple-sdk/releases/download/9.23.0/InApps.xcframework.zip&quot;<br>    }<br>}</pre><p>Since podspec Files are written in Ruby, we were able to parse this JSON data to define podspecs:</p><pre>require &#39;fileutils&#39;<br>require &#39;json&#39;<br>require &#39;ostruct&#39;<br><br>module ReleaseSDK<br>  @@config = JSON.parse(File.read(&#39;package.json&#39;), {object_class: OpenStruct})<br>  def self.config<br>    @@config<br>  end<br><br>  module Spec<br>    def define()<br>      podspec_path = caller.find do |trace|<br>        File.extname(trace.split(&quot;:&quot;)[0]).eql?(&#39;.podspec&#39;)<br>      end.split(&quot;:&quot;)[0]<br><br>      podspec = File.basename(podspec_path, File.extname(podspec_path))<br>      package_index = ReleaseSDK.config.packages.find_index { |package| package.name == podspec }<br>      package = ReleaseSDK.config.packages[package_index] if package_index<br><br>      self.name              = podspec<br>      self.version           = package&amp;.version || ReleaseSDK.config.version<br><br>      if package<br>        self.source       = { :http =&gt; package.url, :sha256 =&gt; package[:hash] }<br>      else<br>        self.source       = {<br>          :git =&gt; &#39;https://github.com/user/apple-sdk.git&#39;,<br>          :tag =&gt; &quot;#{ReleaseSDK.config.tagPrefix}#{self.version.to_s}&quot;<br>        }<br>      end<br><br>      self.ios.deployment_target = &#39;11.0&#39;<br>      self.requires_arc = true<br>      self.preserve_paths = &quot;*.md&quot;, &quot;LICENSE&quot; unless package<br>    end<br>  end<br>end</pre><p>For SPM, this becomes challenging as SPM package manifest file isn’t allowed file system access. To circumvent this limitation, we decided to create a custom script as part of the release process to update the Package.swift file:</p><pre>#!/usr/bin/ruby<br><br># Usage:<br># Update Package.swift file according to package.json<br><br>require &#39;fileutils&#39;<br>require &#39;json&#39;<br>require &#39;ostruct&#39;<br><br>config = JSON.parse(File.read(&#39;package.json&#39;), {object_class: OpenStruct})<br>config_map = Hash[ *config.packages.collect { |package| [ package.name, package ] }.flatten ]<br><br>def binary_target(package)<br>  &quot;.binaryTarget(name: \&quot;#{package.name}\&quot;, url: \&quot;#{package.url}\&quot;, checksum: \&quot;#{package[:hash]}\&quot;)&quot;<br>end<br><br>package_swift = &lt;&lt;PACKAGE<br>// swift-tools-version:5.5<br>// The swift-tools-version declares the minimum version of Swift required to build this package.<br>// This file generated from post_build script, modify the script instead of this file.<br><br>import PackageDescription<br><br>let package = Package(<br>    name: &quot;#{config.name}&quot;,<br>    platforms: [.iOS(.v11), .tvOS(.v11)],<br>    products: [], dependencies: [], targets: [],<br>    swiftLanguageVersions: [.v5]<br>)<br><br>struct PackageProduct {<br>    let name: String<br>    let targets: [Target]<br>}<br><br>extension Collection where Element == Target.Dependency {<br>    static var `default`: [Target.Dependency] {<br>        return [<br>            &quot;Analytics&quot;, &quot;Core&quot;, &quot;Messaging&quot;,<br>            &quot;ObjCUtils&quot;, &quot;SDK&quot;, &quot;Security&quot;,<br>        ]<br>    }<br><br>    static func additional(dependency: Target.Dependency) -&gt; [Target.Dependency] {<br>        var dependencies = Self.default<br>        dependencies.append(dependency)<br>        return dependencies<br>    }<br>}<br><br>let products: [PackageProduct] = [<br>    .init(<br>        name: &quot;iOS-SDK&quot;,<br>        targets: [<br>            #{binary_target(config_map[&#39;Core&#39;])},<br>            #{binary_target(config_map[&#39;Messaging&#39;])},<br>            #{binary_target(config_map[&#39;SDK&#39;])},<br>        ]<br>    ),<br>    .init(<br>        name: &quot;TriggerEvaluator&quot;,<br>        targets: [<br>            #{binary_target(config_map[&#39;TriggerEvaluator&#39;])},<br>            .target(name: &quot;TriggerEvaluatorSPM&quot;, dependencies: .default),<br>        ]<br>    ),<br>    .init(<br>        name: &quot;InApps&quot;,<br>        targets: [<br>            #{binary_target(config_map[&#39;InApps&#39;])},<br>            .target(name: &quot;InAppSPM&quot;, dependencies: .additional(dependency: &quot;TriggerEvaluator&quot;)),<br>        ]<br>    ),<br>]<br><br>for product in products {<br>    for target in product.targets {<br>        package.targets.append(target)<br>    }<br>    package.products.append(<br>        .library(name: product.name, targets: product.targets.map { $0.name })<br>    )<br>}<br><br>PACKAGE<br><br>File.open(&#39;Package.swift&#39;, &#39;w&#39;) do |file|<br>  file.write(package_swift)<br>end</pre><h4>Seamless SDK version management</h4><p>MoEngage provides different functionalities that are distributed across multiple dependencies. This allows customers to integrate features of their choosing without additional cost in terms of app size. Due to the independent development and release of these packages, it becomes a development overhead for our customers in terms of maintaining dependency constraints across different packages.</p><p>We have solved this by shipping the entire SDK with a single version, each package being exposed as a feature that customers can include/exclude. We use subspecs in CocoaPods and package products in Swift Package Manager to achieve this.</p><pre>require &#39;fileutils&#39;<br>require &#39;json&#39;<br>require &#39;ostruct&#39;<br><br>module ReleaseSDK<br>  @@config = JSON.parse(File.read(&#39;package.json&#39;), {object_class: OpenStruct})<br>  def self.config<br>    @@config<br>  end<br><br>  module SubSpec<br>    def dependency_pod(name, proxy = nil)<br>      dependency = ReleaseSDK.config.packages.find { |package| package.name.eql?(name) }<br>      if proxy<br>        self.send(proxy).dependency name, dependency.version<br>      else<br>        self.dependency name, dependency.version<br>      end<br>    end<br>  end<br>end<br><br>Pod::Spec.new do |s|<br>  s.define<br><br>  s.tvos.deployment_target = &#39;11.0&#39;<br>  s.default_subspec = &#39;Core&#39;<br>  s.subspec &#39;Core&#39; do |ss|<br>    ss.extend ReleaseSDK::SubSpec<br>    ss.dependency_pod &#39;SDK&#39;<br>    ss.dependency_pod &#39;Core&#39;<br>    ss.dependency_pod &#39;Messaging&#39;<br>  end<br><br>  s.subspec &#39;InApps&#39; do |ss|<br>    ss.extend ReleaseSDK::SubSpec<br>    ss.dependency &#39;iOS-SDK/Core&#39;<br>    ss.dependency_pod &#39;TriggerEvaluator&#39;<br>    ss.dependency_pod &#39;InApps&#39;<br>  end<br>end</pre><pre>// swift-tools-version:5.5<br>// The swift-tools-version declares the minimum version of Swift required to build this package.<br>// This file generated from post_build script, modify the script instead of this file.<br><br>import PackageDescription<br><br>let package = Package(<br>    name: &quot;iOS-SDK&quot;,<br>    platforms: [.iOS(.v11), .tvOS(.v11)],<br>    products: [], dependencies: [], targets: [],<br>    swiftLanguageVersions: [.v5]<br>)<br><br>struct PackageProduct {<br>    let name: String<br>    let targets: [Target]<br>}<br><br>extension Collection where Element == Target.Dependency {<br>    static var `default`: [Target.Dependency] {<br>        return [<br>            &quot;Analytics&quot;, &quot;Core&quot;, &quot;Messaging&quot;,<br>            &quot;ObjCUtils&quot;, &quot;SDK&quot;, &quot;Security&quot;,<br>        ]<br>    }<br><br>    static func additional(dependency: Target.Dependency) -&gt; [Target.Dependency] {<br>        var dependencies = Self.default<br>        dependencies.append(dependency)<br>        return dependencies<br>    }<br>}<br><br>let products: [PackageProduct] = [<br>    .init(<br>        name: &quot;iOS-SDK&quot;,<br>        targets: [<br>            .binaryTarget(name: &quot;Core&quot;, url: &quot;https://github.com/user/apple-sdk/releases/download/9.23.1/Core.xcframework.zip&quot;, checksum: &quot;bded0a602e5ee8a5bc3705dc2acf115f6f6244014b72f2a476666d0f4bd99d07&quot;),<br>            .binaryTarget(name: &quot;Messaging&quot;, url: &quot;https://github.com/user/apple-sdk/releases/download/9.23.0/Messaging.xcframework.zip&quot;, checksum: &quot;a6d33373f24669462ebac6db35bd7befa077587399ffd1b14143ba453f52c819&quot;),<br>            .binaryTarget(name: &quot;SDK&quot;, url: &quot;https://github.com/user/iOS-SDK/releases/download/9.21.0/SDK.xcframework.zip&quot;, checksum: &quot;25994092c82b0ce008eaf08fefe6964338b9652c93e28087a7adbb752f59fe37&quot;),<br>        ]<br>    ),<br>    .init(<br>        name: &quot;TriggerEvaluator&quot;,<br>        targets: [<br>            .binaryTarget(name: &quot;TriggerEvaluator&quot;, url: &quot;https://github.com/user/iOS-SDK/releases/download/9.21.0/TriggerEvaluator.xcframework.zip&quot;, checksum: &quot;547854ceed99693bede020e18987de36bba56b7334b5ddb0e518b7e6de363e71&quot;),<br>            .target(name: &quot;TriggerEvaluatorSPM&quot;, dependencies: .default),<br>        ]<br>    ),<br>    .init(<br>        name: &quot;InApps&quot;,<br>        targets: [<br>            .binaryTarget(name: &quot;InApps&quot;, url: &quot;https://github.com/user/apple-sdk/releases/download/9.23.0/InApps.xcframework.zip&quot;, checksum: &quot;bc73edbb6438af435272c9f699e995d1d30ff4c11a2efbd5cd741d7e614b6b13&quot;),<br>            .target(name: &quot;InAppSPM&quot;, dependencies: .additional(dependency: &quot;TriggerEvaluator&quot;)),<br>        ]<br>    ),<br>]<br><br>for product in products {<br>    for target in product.targets {<br>        package.targets.append(target)<br>    }<br>    package.products.append(<br>        .library(name: product.name, targets: product.targets.map { $0.name })<br>    )<br>}</pre><p>SPM consumers can include products with a name iOS-SDK, InApps In their app, they target to consume the dependency while having to declare a single repository and version as a dependency constraint. CocoaPods consumers can declare the version once, and use additional subspecs to include additional features:</p><pre>pod &#39;iOS-SDK&#39;,&#39;~&gt; 9.23.1&#39;<br>pod &#39;iOS-SDK/InApps&#39;</pre><p>We publish all our code as XCFrameworks, so our customers’ build performance won’t be impacted. As we support integration both from application target and application extension targets (i.e., notification service extension), we publish the libraries as dynamically linked libraries to avoid an application binary size increase.</p><p>Since customers can’t see or verify the code being integrated, integrating XCFrameworks introduces security risks. To avoid malicious XCFramework being integrated, we follow a two-step verification process.</p><ul><li>For the first step, to allow malicious XCFrameworks being rejected by dependency managers, by specifying SHA256 hash of our XCFramework archive in both SPM and CocoaPods.</li></ul><pre>.binaryTarget(name: &quot;InApps&quot;, url: &quot;https://github.com/user/apple-sdk/releases/download/9.23.0/InApps.xcframework.zip&quot;, checksum: &quot;bc73edbb6438af435272c9f699e995d1d30ff4c11a2efbd5cd741d7e614b6b13&quot;),</pre><pre>self.source = { :http =&gt; package.url, :sha256 =&gt; package[:hash] }</pre><p>The SHA256 hash is generated during our release process from the built xcframework archives:</p><pre>package[:hash] = `shasum -a 256 &quot;XCFramework/#{archive}&quot;`.scan(/\S+/)[0]</pre><ul><li>For the second step, we rely on the verification by the Xcode build system itself. For this, we sign our XCFrameworks using codesign:</li></ul><pre>system(&quot;codesign --timestamp -s \&quot;#{ENV[&#39;CERTIFICATE_IDENTITY&#39;]}\&quot; \&quot;#{xcframework}\&quot;&quot;, out: STDOUT, exception: true)</pre><p>The signature can be verified by clicking on the XCFramework file and viewing <strong>File Inspector</strong> in Xcode:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/256/1*SAIipX18et0RF13xPIxxzg.png" /><figcaption>XCFramework signature</figcaption></figure><h4>Efficient and Reliable Release Process</h4><p>As last part of our goal, we want the release process to as much cost and time efficient while being extremely reliable and less prone to errors. To increase efficiency and reliability, we identified key release processes to optimize and gracefully handle failures:</p><ul><li>Create XCFrameworks</li><li>Publish to CocoaPods</li></ul><p><strong>Create XCFrameworks:</strong> Since we publish multiple XCFrameworks with inter-dependencies, building XCFrameworks per target resulted in dependency modules being built repeatedly causing higher build time.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*paaVC5EX9Y101LFL93RNFQ.png" /><figcaption>Initial MoEngage Apple SDK build process</figcaption></figure><p>We decided to go with a shared scheme that builds all the XCFrameworks together, resulting in significantly less build time. But this resulted in unnecessary XCFrameworks being built without any code changes. We decided to convert each target into CocoaPods podspecs. This gave usthe benefit of re-using already released XCFrameworks with released pods.</p><pre>require &#39;fileutils&#39;<br>require &#39;net/http&#39;<br>require &#39;json&#39;<br>require &#39;ostruct&#39;<br><br>module SDK<br>  XCARCHIVE = &#39;XCArchive&#39;<br>  XCFRAMEWORK = &#39;XCFramework&#39;<br>  EXAMPLES = &#39;Examples&#39;<br>  PACKAGE_CONFIG = &#39;package.json&#39;<br>  DERIVED_DATA = File.expand_path(&#39;~/Library/Developer/Xcode/DerivedData&#39;)<br>  XCArchive = Struct.new(:scheme, :destination, :path, :ios)<br>end<br><br># Set up `pods.rb` file for source of pods.<br>#<br># Use cache for frameworks not to be built<br>#<br># @param [Array] Frameworks to be built from source.<br># @param [Array] Cached Frameworks to be used.<br>def setup_pods(build_frameworks, cached_frameworks)<br>  build_framework_pod = proc do |framework|<br>    &quot;pod &#39;#{framework.name}&#39;, :path =&gt; &#39;../&#39;&quot;<br>  end<br><br>  cached_framework_pod = proc do |framework|<br>    &quot;pod &#39;#{framework.name}&#39;, &#39;#{framework.version}&#39;&quot;<br>  end<br><br>  ios_pods = build_frameworks.map(&amp;build_framework_pod) + cached_frameworks.map(&amp;cached_framework_pod)<br>  extension_pods, ios_pods = ios_pods.partition { |syntax| syntax.include?(&#39;RichNotification&#39;) }<br>  tv_pods = build_frameworks.select(&amp;:common).map(&amp;build_framework_pod) + cached_frameworks.select(&amp;:common).map(&amp;cached_framework_pod)<br>  injected_pods = &lt;&lt;EOF<br>def use_custom_pods<br>  #{ios_pods.join(&quot;\n  &quot;)}<br>end<br><br>def use_custom_extension_pods<br>  #{extension_pods.join(&quot;\n  &quot;)}<br>end<br><br>def use_custom_tv_pods<br>  #{tv_pods.join(&quot;\n  &quot;)}<br>end<br>EOF<br>  File.open(File.join(SDK::EXAMPLES, &#39;pods.rb&#39;), &#39;w&#39;) do |file|<br>    file.write(injected_pods)<br>  end<br>  Dir.chdir(SDK::EXAMPLES) { system(&#39;pod install&#39;, out: STDOUT, exception: true) }<br>end<br><br># Create XCArchives to extract XCFrameworks.<br>#<br># @param [Bool] Whether to build for tvOS as well.<br>def create_archives(tv)<br>  workspace_dir = File.join(SDK::EXAMPLES, &#39;Examples.xcworkspace&#39;)<br>  archives = [<br>    SDK::XCArchive.new(&#39;All iOS&#39;, &#39;generic/platform=iOS&#39;, File.join(SDK::XCARCHIVE,&#39;All-iOS.xcarchive&#39;), true),<br>    SDK::XCArchive.new(&#39;All iOS&#39;, &#39;generic/platform=iOS Simulator&#39;, File.join(SDK::XCARCHIVE,&#39;All-iOS-Sim.xcarchive&#39;), true),<br>  ]<br>  # only builds tvOS archive if any framework supports tvOS<br>  archives.push(<br>    SDK::XCArchive.new(&#39;All tvOS&#39;, &#39;generic/platform=tvOS&#39;, File.join(SDK::XCARCHIVE,&#39;All-tvOS.xcarchive&#39;), false),<br>    SDK::XCArchive.new(&#39;All tvOS&#39;, &#39;generic/platform=tvOS Simulator&#39;, File.join(SDK::XCARCHIVE,&#39;All-tvOS-Sim.xcarchive&#39;), false)<br>  ) if tv<br>  archives.each do |archive|<br>    system(&quot;xcodebuild archive -workspace \&quot;#{workspace_dir}\&quot; -scheme \&quot;#{archive.scheme}\&quot; -destination \&quot;#{archive.destination}\&quot; -archivePath \&quot;#{archive.path}\&quot; SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES&quot;, out: STDOUT, exception: true)<br>  end<br>  return archives<br>end<br><br># Build XCFrameworks for provided frameworks.<br>#<br># @param [Array] Frameworks to build for.<br>def build_xcframeworks(frameworks)<br>  # cleanup previous XCFrameworks and XCArchives<br>  [SDK::XCFRAMEWORK, SDK::XCARCHIVE].each { |dir| FileUtils.rm_rf(dir) if File.directory?(dir) }<br><br>  # create XCFramework and XCArchive directories<br>  FileUtils.mkdir_p(SDK::XCARCHIVE)<br>  FileUtils.mkdir_p(SDK::XCFRAMEWORK)<br><br>  # get frameworks to be built metadata<br>  package = JSON.parse(File.read(SDK::PACKAGE_CONFIG), {object_class: OpenStruct})<br>  build_framework_names = frameworks.map { |arg| arg.strip }<br>  build_frameworks, cached_frameworks = package.frameworks.partition do |framework|<br>    build_framework_names.include?(framework.name)<br>  end<br><br>  if build_frameworks.empty?<br>    puts &quot;No framework passed to build: #{frameworks}&quot;<br>    return<br>  end<br><br>  # optimize build using cached frameworks<br>  setup_pods(build_frameworks, cached_frameworks)<br><br>  # create XCArchives and XCFrameworks<br>  create_xcframeworks(build_frameworks)<br><br>  # print SHA256 for generated zips<br>  Dir.glob(&quot;#{SDK::XCFRAMEWORK}/**/*.xcframework.zip&quot;).each do |zip|<br>    system(&quot;shasum -a 256 \&quot;#{zip}\&quot;&quot;, out: STDOUT, exception: true)<br>  end<br>end<br><br># Create XCFrameworks for provided frameworks.<br>#<br># Builds, signs and zips XCFrameworks.<br>#<br># @param [Array] Frameworks to create for.<br>def create_xcframeworks(frameworks)<br>  # delete derived data<br>  FileUtils.rm_rf(SDK::DERIVED_DATA) if File.directory?(SDK::DERIVED_DATA)<br><br>  archives = create_archives(frameworks.any?(&amp;:common))<br>  frameworks.each do |framework|<br>    # create flags for create-xcframework command<br>    flags = archives.map do |archive|<br>      next nil if !framework.common &amp;&amp; !archive.ios<br>      framework_file = File.join(archive.path, &#39;Products&#39;, &#39;Library&#39;, &#39;Frameworks&#39;, &quot;#{framework.name}.framework&quot;)<br>      dsym = File.join(Dir.pwd, archive.path, &#39;dSYMs&#39;, &quot;#{framework.name}.framework.dSYM&quot;)<br>      &quot;-framework \&quot;#{framework_file}\&quot; -debug-symbols \&quot;#{dsym}\&quot;&quot;<br>    end.compact<br><br>    # create xcframework<br>    xcframework = File.join(SDK::XCFRAMEWORK, &quot;#{framework.name}.xcframework&quot;)<br>    system(&quot;xcodebuild -create-xcframework #{flags.join(&#39; &#39;)}  -output \&quot;#{xcframework}\&quot;&quot;, out: STDOUT, exception: true)<br><br>    # sign xcframework<br>    system(&quot;codesign --timestamp -s \&quot;#{ENV[&#39;MO_CERTIFICATE_IDENTITY&#39;]}\&quot; \&quot;#{xcframework}\&quot;&quot;, out: STDOUT, exception: true)<br><br>    # create xcframework zip<br>    Dir.chdir(SDK::XCFRAMEWORK) {<br>      xcframework_name = File.basename(xcframework)<br>      system(&quot;zip -r #{xcframework_name}.zip #{xcframework_name}/ -x \&quot;*.DS_Store\&quot;&quot;, out: STDOUT, exception: true)<br>    }<br>  end<br>end</pre><p>Similar to our distribution model discussed above sections, we created a package.json file to store common configurations regarding the frameworks i.e., target name, target tag prefix, and current release version, target supported platforms etc. This file is used in individual podspec files, which in turn are used as local pods:</p><pre>source = {:path =&gt; &#39;../&#39;}<br>project_dir = File.dirname(__FILE__)<br><br>abstract_target &#39;Workspace&#39; do<br>  platform :ios, &#39;13.0&#39;<br><br>  # Pods for Workspace<br>  if File.file?(File.join(project_dir, &#39;pods.rb&#39;)) # use CI/CD specific sources<br>    require_relative File.join(project_dir, &#39;pods.rb&#39;)<br>    use_custom_pods<br>  else # use default local sources<br>    pod &#39;Core&#39;, :testspecs =&gt; [&#39;Tests&#39;], **source<br>  end<br><br>  target &#39;Test&#39; do<br>    use_frameworks!<br><br>    # Pods for MoETest<br>    pod &#39;SwiftLint&#39;, &#39;0.57.0&#39;<br>  end<br><br>  target &#39;SwiftUITest&#39; do<br>    use_frameworks!<br><br>    # Pods for MoESwiftUITest<br>    pod &#39;SwiftLint&#39;, &#39;0.57.0&#39;<br>  end<br><br>  target &#39;NotificationContent&#39; do<br>    use_frameworks!<br>    inherit! :search_paths<br><br>    # Pods for NotificationContent<br>    if File.file?(File.join(project_dir, &#39;pods.rb&#39;)) # use CI/CD specific sources<br>      require_relative File.join(project_dir, &#39;pods.rb&#39;)<br>      use_custom_extension_pods<br>    else # use default local sources<br>      pod &#39;RichNotification&#39;, :testspecs =&gt; [&#39;Tests&#39;], **source<br>    end<br>  end<br><br>  target &#39;NotificationService&#39; do<br>    use_frameworks!<br>    inherit! :search_paths<br><br>    # Pods for NotificationService<br>    if File.file?(File.join(project_dir, &#39;pods.rb&#39;)) # use CI/CD specific sources<br>      require_relative File.join(project_dir, &#39;pods.rb&#39;)<br>      use_custom_extension_pods<br>    else # use default local sources<br>      pod &#39;RichNotification&#39;, :testspecs =&gt; [&#39;Tests&#39;], **source<br>    end<br>  end<br>end</pre><p>With this change our XCFramework build pipeline was optimized to only build frameworks that contain code changes, using released version of frameworks that doesn’t have any change and hence require no release.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*H6GuvZQ8LYW1Ez8beDRKXw.png" /><figcaption>Final MoEngage Apple SDK build process</figcaption></figure><p><strong>Publish to CocoaPods:</strong> As last part of our release process we have to publish new pods to CocoaPods trunk. In CocoPods publish process, CocoPods validates the pod is valid and its dependencies are already available in CocoaPods trunk. To pass this validation, we publish the dependency pods first and then switching to dependent pods. CocoPods recommends — synchronous flag when publishing pod that has dependency pod published immediately before.</p><p>The temporary projects created by CocoaPods for pod validations consume higher amount of storage as the number of pods published increases. We decided to clear derived data and temporary directories after each pod is published to not hit GitHub runners’ storage limit.</p><p><a href="https://github.com/CocoaPods/trunk.cocoapods.org/issues/207">CocoaPods also has internal data race where pods get partially published making the pod unusable for consumers.</a> We added handling to remove and retry publishing such pods after a certain cooldown when such a failure happens:</p><pre>#!/usr/bin/ruby<br><br># Usage:<br># Publish pods to CocoaPods<br><br>require &#39;fileutils&#39;<br>require &#39;json&#39;<br>require &#39;ostruct&#39;<br><br>podspec = &quot;#{package.name}.podspec&quot;<br>info = `pod trunk info #{package.name}`<br>next if !File.exist?(podspec) || info.include?(&quot; #{package.version} &quot;)<br>puts &quot;::group::Publishing #{podspec}&quot;<br>begin<br>  FileUtils.rm_rf(derived_data) if File.directory?(derived_data)<br>  system(&quot;pod trunk push \&quot;#{podspec}\&quot; --verbose --synchronous --allow-warnings&quot;, out: STDOUT, exception: true)<br>  sleep 10 # even with --synchronous flag push might fail for depending pods<br>rescue =&gt; error<br>  sleep 60 # wait for timeout error<br>  info = `pod trunk info #{package.name}`<br>  next if info.include?(&quot; #{package.version} &quot;) # skip if pod is already published<br>  puts &quot;::warning::Pod published failed with #{error&amp;.message}, trying publish again&quot;<br>  begin<br>    # remove pod to avoid conflict with existing pod<br>    # https://github.com/CocoaPods/trunk.cocoapods.org/issues/207<br>    system(&quot;pod trunk delete #{package.name} #{package.version} --verbose&quot;, out: STDOUT, exception: false)<br>  rescue =&gt; error<br>    puts &quot;::warning::Pod delete failed with #{error&amp;.message}, trying publish again&quot;<br>  end<br>  system(&quot;pod trunk push \&quot;#{podspec}\&quot; --verbose --synchronous --allow-warnings&quot;, out: STDOUT, exception: true)<br>  sleep 10 # even with --synchronous flag push might fail for depending pods<br>end<br>puts &quot;::endgroup::&quot;</pre><p><a href="https://blog.cocoapods.org/CocoaPods-Specs-Repo/">With the recent CocoaPods trunk read only plan</a>, we added to our own custom specs repo to our publish action. We share lot of code between podspecs, these reusable code are split into multiple files. We publish the podspecs as JSON to avoid any import errors due to relative path mismatch:</p><pre>repo_info = `pod repo list`<br>if !repo_info.include?(&#39;specs&#39;)<br>  system(&quot;pod repo add specs https://github.com/user/PodSpecs.git&quot;, out: STDOUT, exception: true)<br>end<br><br>puts &quot;::group::Publishing #{podspec} to specs&quot;<br>system(&quot;pod repo push specs \&quot;#{podspec}\&quot; --verbose --allow-warnings --skip-import-validation --use-json&quot;, out: STDOUT, exception: true)<br>puts &quot;::endgroup::&quot;</pre><p>Even after CocoaPods trunk is made read-only, our customers can still use our pods just by adding a single line to their Podfile:</p><pre>source &#39;https://github.com/user/PodSpecs.git&#39;</pre><p>In this article, we have explored the fundamentals of creating Apple SDK deployment pipelines.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3d4da6b915b4" width="1" height="1" alt=""><hr><p><a href="https://medium.com/life-at-moengage/building-deployment-pipelines-for-mobile-sdks-part-3-3d4da6b915b4">Building Deployment Pipelines for Mobile SDKs — Part 3</a> was originally published in <a href="https://medium.com/life-at-moengage">MoEngage</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Ultimate Guide to Debugging iOS App Storage]]></title>
            <link>https://soumyamahunt.medium.com/ultimate-guide-to-debugging-ios-app-storage-159b0c798f82?source=rss-41d824adb39d------2</link>
            <guid isPermaLink="false">https://medium.com/p/159b0c798f82</guid>
            <category><![CDATA[ios-app-development]]></category>
            <category><![CDATA[apple]]></category>
            <category><![CDATA[ios-development]]></category>
            <category><![CDATA[sql]]></category>
            <category><![CDATA[ios]]></category>
            <dc:creator><![CDATA[Soumya Mahunt]]></dc:creator>
            <pubDate>Tue, 13 Jan 2026 16:08:37 GMT</pubDate>
            <atom:updated>2026-01-13T16:08:37.615Z</atom:updated>
            <content:encoded><![CDATA[<h4>Identify and fix storage bloating, corrupted data, and stale caches in your app</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9p3bo9KzV7nimDibhWr9og.jpeg" /></figure><p>When you suspect an iOS app is behaving unexpectedly due to an invalid storage state — such as corrupted files, stale caches, unusual high storage space, or leftover temporary data — one of the most direct ways to investigate is to take a backup of the device and inspect your app’s sandbox from that backup.</p><p>Apple iOS backups made via Finder or iTunes contain all the files from each app’s sandbox — but arranged in a special hashed folder structure. With some SQLite queries, you can extract and inspect exactly what’s in your app without needing third‑party tools. <strong>This approach allows you to view app-group directory contents as well, which the <em>Xcode’s Download container…</em> doesn’t show.</strong></p><h3>Why Backup-Based Inspection Helps</h3><p>On a physical iOS device, the app’s sandbox contains Documents, Library, Caches, and other directories, but they aren’t easily accessible without jailbreaking. The simulator’s environment is different and might not reflect your production storage usage.</p><p>By taking an unencrypted backup, you can navigate the actual app container from a real device and identify what’s causing your app to misbehave.</p><h3>1. Prepare the Device</h3><p>Reproduce the storage issue on your test or user device so the backup will include the state you want to debug.</p><h3>2. Take an Unencrypted Backup</h3><p>Use Finder or iTunes:</p><ul><li>Connect your device via USB.</li><li>In <strong>Finder/iTunes</strong>, select your device.</li><li>Select <strong>Back up all the data on your iPhone to this Mac</strong>.</li><li>Choose <strong>Back Up Now</strong>.</li><li>Ensure <strong>“Encrypt local backup”</strong> is unchecked.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2UZxr2W8aGgvHyICU9ckxA.png" /><figcaption>Local backup screen</figcaption></figure><p>Encrypted backups store data in a different way and require extra steps to inspect.</p><h3>3. Locate the Backup Folder</h3><p>Once done, the backup is stored in:</p><ul><li>macOS: ~/Library/Application Support/MobileSync/Backup/</li><li>Windows: \Users\&lt;username&gt;\AppData\Roaming\Apple Computer\MobileSync\Backup\</li></ul><p>Inside you’ll see one or more directories named with long hex strings (the device UDID). Each directory represents a backup session. You can directly navigate to particular backup by:</p><ul><li>Click on <strong>Manage Backups…</strong></li><li><strong>Right click</strong> on individual backup.</li><li>Click <strong>Show in Finder</strong>.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6DNNxSG_wmgfBvLN-r8ROA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*G2HS5NJQQtwba5W3amA3ew.png" /><figcaption>Managing backup screen</figcaption></figure><h3>📂 Understanding iOS Backup Folder Structure</h3><p>Unlike normal file systems, Apple’s backup folders do not store files with their original paths or names. Instead, every original file path is mapped to a SHA1-like hash name.</p><p>The mapping is stored in a SQLite database file called Manifest.db at the root of the backup folder. Backup folder contents typically look like this:</p><pre>&lt;UDID&gt;/<br>├── Info.plist<br>├── Manifest.db<br>├── Manifest.plist<br>├── Status.plist<br>├── 0a/0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a<br>├── 0b/0b0b0b…<br>├── …</pre><p>The subfolders (0a, 0b, etc.) are based on the first two characters of the hashed filename.</p><h3>📑 Inside Manifest.db</h3><p>Manifest.db is a SQLite database that maps original file paths inside the app sandbox to backup filenames.</p><p>The main table of interest is Files. Typical columns include:</p><ul><li>fileID — The hashed filename used in backup storage (this is the actual file in subfolders).</li><li>domain — The logical container name, typically AppDomain-&lt;bundle-id&gt; for your app.</li><li>relativePath — Path within the app’s sandbox, e.g., Library/Caches/image.png.</li><li>flags — Indicators for file type and attributes.</li></ul><h3>🔍 Querying Manifest.db with SQLite</h3><p>You can use the sqlite3 command-line tool (macOS has it built in).</p><p><strong><em>Example: Find a specific file in the backup from app’s sandbox<br>Replace </em></strong><strong><em>com.yourcompany.yourapp with your app bundle id</em></strong></p><pre>sqlite3 Manifest.db &quot;SELECT fileID, relativePath FROM Files WHERE domain = &#39;AppDomain-com.yourcompany.yourapp&#39; AND relativePath LIKE &#39;%mydatabase.sqlite%&#39;;&quot;</pre><p><strong><em>Example: Find a specific file in the backup from app group location<br>Replace </em></strong><strong><em>group.com.yourcompany.yourappgroup with your app group</em></strong></p><pre>sqlite3 Manifest.db &quot;SELECT fileID, relativePath FROM Files WHERE domain = &#39;AppDomainGroup-group.com.yourcompany.yourappgroup&#39; AND relativePath LIKE &#39;%mydatabase.sqlite%&#39;;&quot;</pre><blockquote><strong>In case you are running into access issue while querying </strong><strong>Manifest.db, you can copy the </strong><strong>Manifest.db file to folder you already have access to and run </strong><strong>sqlite3 commands from there:</strong></blockquote><pre>cp &quot;~/Library/Application Support/MobileSync/Backup/&lt;UDID&gt;/Manifest.db&quot; &quot;~/Documents/Manifest.db&quot;<br>cd ~/Documents<br>sqlite3 Manifest.db &quot;&lt;query&gt;&quot;</pre><h3>🗂 Extracting and Viewing File Content</h3><p>Once you have the fileID from your query:</p><ol><li>The backup file is stored in a subfolder named after the first two characters of fileID.</li><li>The file’s name in the backup is exactly the fileID string.</li></ol><p>Example:</p><pre>FILEID=&quot;0a123456789abcdef…&quot;<br>cp &quot;$(echo $FILEID | cut -c1-2)/$FILEID&quot; ~/Desktop/extracted_file</pre><p>The copied file is now in its raw form and can be opened with the appropriate viewer:</p><ul><li>.sqlite — open with <a href="https://sqlitebrowser.org/">DB Browser for SQLite</a></li><li>.jpg / .png — open in Preview</li><li>.json / .plist — view with a text editor or Xcode</li></ul><h3>Automating the Process</h3><p>You can script extraction of all files in your app:</p><pre>APPDOMAIN=&quot;AppDomain-com.yourcompany.yourapp&quot;<br>sqlite3 Manifest.db &quot;SELECT fileID, relativePath FROM Files WHERE domain = &#39;$APPDOMAIN&#39;;&quot; | while IFS=&#39;|&#39; read FILEID RELATIVEPATH; do<br>  SUBDIR=$(echo $FILEID | cut -c1-2)<br>  SOURCE=&quot;$SUBDIR/$FILEID&quot;<br>  DEST=&quot;~/Desktop/backup_extract/$RELATIVEPATH&quot;<br>  if [ -f &quot;$SOURCE&quot; ]; then<br>    mkdir -p &quot;$(dirname &quot;$DEST&quot;)&quot;<br>    cp &quot;$SOURCE&quot; &quot;$DEST&quot;<br>  fi<br>done</pre><p>This will recreate your app’s sandbox folder on your Desktop from the backup data.</p><blockquote><strong>In case you are running into access issue, you can copy the backup folder to a folder you already have access to and run the script from this new folder location.</strong></blockquote><h3>⚠️ Security Notes</h3><ul><li>Backups contain sensitive user files: handle with care.</li><li>Only debug using test devices or data with user consent.</li><li>Avoid uploading extracted data to cloud storage unless anonymized.</li></ul><h3>Conclusion</h3><p>Inspecting iOS backups via Manifest.db gives you full visibility into how your app is using storage on a real device. By linking the hashed filenames in the backup to their original paths, you can extract caches, databases, and media files to understand and fix excessive storage usage.</p><p>This method is fully native — no third‑party tools required, only Finder/iTunes and SQLite — and is a key skill for iOS developers debugging storage-related issues.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=159b0c798f82" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Debugging Binaries in Xcode]]></title>
            <link>https://soumyamahunt.medium.com/debugging-binaries-in-xcode-c40625a2ed5b?source=rss-41d824adb39d------2</link>
            <guid isPermaLink="false">https://medium.com/p/c40625a2ed5b</guid>
            <category><![CDATA[xcode]]></category>
            <category><![CDATA[ios-app-development]]></category>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[debugging]]></category>
            <category><![CDATA[ios]]></category>
            <dc:creator><![CDATA[Soumya Mahunt]]></dc:creator>
            <pubDate>Tue, 02 Dec 2025 13:55:05 GMT</pubDate>
            <atom:updated>2025-12-02T13:55:05.030Z</atom:updated>
            <content:encoded><![CDATA[<h4>Attach, Break, Remap — power tricks every advanced developer should know</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wDkPfT3Xvt54OxGHMNtRGw.png" /></figure><p>When we think about “debugging” we often imagine compiling our own source code in Xcode, setting breakpoints, and stepping through methods line-by-line.</p><p>But sometimes you’ll need to debug <strong>build not generated by you</strong> or <strong>without access to the original project source code </strong>— you might only have the compiled <strong>binary</strong>.</p><p>This is common in scenarios such as:</p><ul><li>Investigating an edge case bug that is only reproducible in an existing developer or QA installation.</li><li>Debugging an <strong>.xcframework/</strong><strong>.framework </strong>that has separate code base from your app.</li></ul><p>A <strong>binary</strong> in this context is machine code generated by Xcode or another compiler, packaged for execution. Examples:</p><ul><li><strong>.ipa</strong>: Packaged iOS application, containing the compiled executable and resources.</li><li><strong>.xcframework/</strong><strong>.framework</strong>: Bundle containing multiple/single binary slices for different architectures (e.g., arm64 for device, x86_64 and arm64 for Simulator).</li><li>Mac <strong>.app </strong>bundle or <strong>.dylib</strong> dynamic library.</li></ul><p>Why debug a binary? You can inspect runtime conditions, test logic paths, and identify faults without rebuilding and reinstalling — as long as the app is correctly signed and you have the <strong>matching source</strong> files.</p><h3>Attaching debugger to process</h3><p>Xcode gives you multiple ways to connect its debugger to a process that is already running on your device or simulator.</p><p>This is essential when you:</p><ul><li>Launch the app outside Xcode (e.g. from App Library, or the command line).</li><li>Start a background process or extension manually.</li><li>Attach to a helper daemon or secondary executable.</li></ul><h4>Method 1: Attach to Process (GUI)</h4><ol><li>Open Xcode.</li><li>Select device on which process is running.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ptTEDydl74OIQfN9zFiqvA.png" /></figure><p>3. From the top menu, select <strong>Debug &gt; Attach to Process</strong>.</p><p>4. A list of currently running processes will appear (on your connected device or Mac).</p><p>5. Click the target process name to attach.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gOeBhWuASDPS4lLYdp_JKg.png" /><figcaption>Attach to Process</figcaption></figure><h4>Method 2: Attach to Process by PID or Name</h4><ol><li>From the menu, choose <strong>Debug &gt; Attach to Process by PID or Name</strong>.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vFKVDifylxkEmuFTMKf9iw.png" /></figure><p>2. Enter either:</p><p>— <strong>PID</strong>: numeric process ID visible via ps command or Instruments.<br> — <strong>Name</strong>: executable name (case-sensitive).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YQLESBD7jjoDCbpU5D3QKQ.png" /></figure><p>3. Xcode will connect to the process if code-signing and entitlements allow.</p><ul><li><strong>Pro Tip: </strong>Use executable name to attach to a process that will be launched in future. Give your process unique name to avoid conflict with other app or extension processes.</li></ul><h3>Setting breakpoints</h3><p>Once attached, breakpoints let you pause execution at specific points and inspect variables, call stacks, registers, and more. Even without access to source code or gutter you can still set breakpoints via Xcode or LLDB command line interface.</p><h4>Symbolication &amp; dSYMs</h4><p>A symbolicated binary has mappings from compiled machine instructions back to human-readable names and source locations. These mappings are stored in dSYM files generated during compilation.<br>Without proper symbol files:</p><ul><li>Function names appear as raw memory addresses.</li><li>You can still debug, but you’ll rely on disassembly and <a href="https://ronyfadel.github.io/swiftdemangler/">manual symbol demangling</a>.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yqdwUWskbYL6rupyJmXoeA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*peXVbuNRESBXNoY2moV4zA.png" /><figcaption>Unsymbolicated vs Symbolicated stack trace</figcaption></figure><h4>Breakpoints in Xcode</h4><p>After attaching, you can create symbolic breakpoint:</p><ol><li>Navigate to <strong>Breakpoint Navigator (</strong><strong>⌘8)</strong></li><li>Click on the <strong>Create a Breakpoint</strong> action button at the bottom</li><li>Click on <strong>Symbolic Breakpoint</strong> or other well known breakpoints (e.g. <strong>Swift Error Breakpoint</strong>)</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/682/1*C-8bq8sBlBbm18nc1NkqvQ.png" /><figcaption>Symbolic Breakpoint Creation</figcaption></figure><h4>Breakpoints via LLDB</h4><p>LLDB gives fine-grained control over breakpoint creation through its command line interface:</p><blockquote>(lldb) breakpoint set — name MyFunction</blockquote><blockquote>(lldb) breakpoint set — file MyClass.swift — line 42</blockquote><blockquote>(lldb) breakpoint set — address 0x1045a3b20</blockquote><p>This is useful when debugging partial source or third-party code where only certain files are available or you need line/column based breakpoints instead of symbolic ones.</p><p><strong>Pro Tip: </strong>Even without source, you can place breakpoints on symbol names extracted via:</p><blockquote>(lldb) image lookup -n MyFunction</blockquote><p>or</p><blockquote>(lldb) image dump symtab MyFramework</blockquote><p><strong>Bonus Tip:</strong> You can also debug source code that Xcode doesn’t support. (e.g. <a href="https://kotlinlang.org/docs/native-debugging.html">Kotlin native in Kotlin Multi-platform use-cases</a>)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*m_PWADZ-r_ZtGIdSzLmfhA.png" /><figcaption>LLDB breakpoint set and hit</figcaption></figure><h4>Map to your source code</h4><p>While you don’t necessarily need access to source code to debug, but having access to code does make debugging and possible fix simpler.</p><p>When a binary is built, dSYMs record absolute paths to source files from the build environment.</p><p>If your local paths differ from those recorded, Xcode/LLDB can’t open files when a breakpoint is hit.</p><p>Use LLDB’s target.source-map to remap locations for this session:</p><blockquote>(lldb) settings set target.source-map /Users/buildmachine/workspace/project/ ~/projects/debug/</blockquote><p>This tells LLDB:<br><strong><em>“Whenever you look for files under </em></strong><strong><em>/Users/buildmachine/workspace/project/, actually look in </em></strong><strong><em>~/projects/debug/ instead.&quot;</em></strong></p><p><strong>Pro Tip:</strong> <a href="https://lldb.llvm.org/man/lldb.html#configuration-files">Add your </a><a href="https://lldb.llvm.org/man/lldb.html#configuration-files">target.source-map in your .lldbinit file to automatically run these commands when lldb is started.</a></p><p><strong>Why this matters: </strong>Without remapping, you can only debug disassembly instead of source code. Correct mapping restores full source view in Xcode.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yqdwUWskbYL6rupyJmXoeA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*peXVbuNRESBXNoY2moV4zA.png" /><figcaption>Unmapped Source vs Mapped Source Debugging</figcaption></figure><p>Reference: <a href="http://lldb.llvm.org/use/map.html#remap-source-file-pathnames-for-the-debug-session">LLDB Source Remapping</a></p><h3>Conclusion</h3><p>Debugging binaries is a powerful skill for diagnosing production issues or analyzing third-party code.</p><p>By using Xcode’s attach capabilities, mastering LLDB breakpoints, and remapping source paths, you can get deep insight into how an app is executing — even without the original project.</p><p>Pair that with secure entitlement management and you’ll have the ultimate reverse-debugging toolkit.</p><p>💬 What’s your most challenging “debug without source” moment? Share in the comments!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c40625a2ed5b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[What you need to know before migrating to Swift Testing]]></title>
            <link>https://soumyamahunt.medium.com/what-you-need-to-know-before-migrating-to-swift-testing-b9c1d749ebd5?source=rss-41d824adb39d------2</link>
            <guid isPermaLink="false">https://medium.com/p/b9c1d749ebd5</guid>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[xctest]]></category>
            <category><![CDATA[swift-testing]]></category>
            <category><![CDATA[swift-concurrency]]></category>
            <category><![CDATA[ios-app-development]]></category>
            <dc:creator><![CDATA[Soumya Mahunt]]></dc:creator>
            <pubDate>Tue, 08 Jul 2025 13:31:23 GMT</pubDate>
            <atom:updated>2025-07-14T14:08:27.265Z</atom:updated>
            <content:encoded><![CDATA[<h4>Migrating from XCTest to Swift Testing: Practical Tips and Insights</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/735/1*CVg1a8Xdtrnn0wvgJT48BA@2x.png" /></figure><p>The evolution of Swift has brought swifter lanes for testing, with Swift Testing opening a new set of possibilities for unit, integration, and even functional testing in your apps. However, making the leap from XCTest to Swift Testing isn’t always straightforward. While Apple provides an official <a href="https://developer.apple.com/documentation/testing/migratingfromxctest">Migration Guide</a>, this post isn’t here to rehash what’s outlined there. Instead, I focus on practical tips and insights I’ve gathered during my own migration journey — things that the official guide doesn’t dive into.</p><p>If you’re planning or are midway through migrating from XCTest to Swift Testing, this post is for you. By the end, you’ll better understand how Swift Testing operates under the hood, and how to navigate some of the intricacies and quirks that come with its adoption.</p><h4>Swift Testing: Discovering and Executing Tests</h4><p>Before we dive into the migration gotchas, it’s important to understand how Swift Testing discovers and executes its tests. XCTest follows a class-based approach, where each XCTestCase subclass serves as a container for test methods. XCTest framework discovers the tests by collecting all the XCTestCase classes and their methods with test prefix, all this are done with the help of objective-C runtime features. Swift Testing, on the other hand, takes a more Swift based approach of using macros to generate test description.</p><p>Test execution also differs in XCTest and Swift Testing. XCTest executes all the tests serially one by one, starting the next test only after the previous one is finished. This behaviour is applicable to async test methods as well. Swift Testing takes an approach based on Swift’s structured concurrency. Each test method under Swift Testing are executed concurrently, by creating a new @Suite instance each time and calling the test method on it inside a new task.</p><p>While these approaches align well with Swift’s evolution toward a functional and composable design, it introduces several challenges — challenges you are likely to encounter during the migration. Let’s explore some of them.</p><h4>Challenge #1: Assertions Don’t Work Outside of a Task Context</h4><p>One of the most significant changes in Swift Testing is that each test is executed as part of a separate task, hence assertions are only valid <a href="https://developer.apple.com/documentation/testing/test/current#discussion">if any test is associated to the task context</a> that the assertion is invoked on. Hence, any assertions that is done outside of test task context in XCTest, needs to be moved to test task itself. XCTest doesn’t have this limitation as there is only one test that is executed at any time in a single process, while Swift Testing prioritises in-process concurrent test execution.</p><p>For example, following code under XCTest:</p><pre>func testExample() {<br>    DispatchQueue.main.async {<br>        // Testing a method that needs to be executed on a specifc queue<br>        let result = systemUnderTest()<br>        XCTAssertEqual(result, 4) // ERROR: Assertion outside of a task context.<br>    }<br>}</pre><p>needs to be migrated to this under Swift Testing:</p><pre>@Test<br>func example() async {<br>    let result = await withCheckedContinuation { continuation in<br>        DispatchQueue.main.async {<br>            // Testing a method that needs to be executed on a specifc queue<br>            let result = systemUnderTest()<br>            // #expect(result == 4) ❌ Outside of test task context<br>            continuation.resume(returning: result)<br>        }<br>    }<br>    #expect(result == 4) // ✅ Valid as back in test task context<br>}</pre><p><strong>Why This Matters</strong></p><p>This requirement can feel cumbersome, especially when migrating tests for code that hasn’t been migrated to Swift Structured Concurrency. Plan to refactor your test structure to ensure assertions occur within the proper task-boundary.</p><p><strong>Recommendation</strong></p><ul><li>Avoid using concurrency paradigms, that are not in aligned with Swift’s Structured Concurrency in Swift Testing. Make sure all the Swift Testing APIs are used in the test task context.</li></ul><h4>Challenge #2: Shared Mutable State Issues</h4><p>In Swift Testing, concurrency is a first-class citizen. While this is great for testing modern async-heavy codebases, it can lead to subtle issues with shared mutable state if you’re not careful.</p><p>Consider a shared resource being accessed by multiple tests:</p><pre>actor Counter {<br>    static let shared = Counter()<br><br>    var count = 0<br>    func increment() { count += 1 }<br>    func decrement() { count -= 1 }<br>}<br><br>@Test(&quot;Test increment&quot;)<br>func inrement() async throws {<br>    print(&quot;Started increment check&quot;)<br>    await Counter.shared.increment()<br>    print(&quot;Sleeping increment check&quot;)<br>    try await Task.sleep(nanoseconds: 3_000_000_000)<br>    print(&quot;Completed Sleeping increment check&quot;)<br>    await #expect(Counter.shared.count == 1)<br>    print(&quot;Finished increment check&quot;)<br>}<br><br>@Test(&quot;Test decrement&quot;)<br>func decrement() async throws {<br>    print(&quot;Started decrement check&quot;)<br>    print(&quot;Sleeping decrement check&quot;)<br>    try await Task.sleep(nanoseconds: 1_000_000_000)<br>    await Counter.shared.decrement()<br>    print(&quot;Completed Sleeping decrement check&quot;)<br>    await #expect(Counter.shared.count == -1)<br>    print(&quot;Finished decrement check&quot;)<br>}</pre><p><strong>Why This Matters</strong></p><p>By default, test tasks are not serialized, leading to the possibility of data races or undefined shared state behaviour. This is less common in XCTest, where tests often run sequentially, even when enabling parallel runs tests are run in parallel with separate processes where they won’t be able to access state of another process.</p><p><strong>Recommendation</strong></p><ul><li>Create a .serialized tagged parent test suite that contains child test suites with tests that are required to be run sequentially. This will make sure tests that can be run concurrently, an be executed concurrently.</li></ul><pre>@Suite(.serialized)<br>struct SerializedTests {<br>    struct Suite1 {<br>        @Test<br>        func stuff() async throws {<br>            // Execute test code<br>        }<br>    }<br>}<br><br>extension SerializedTests {<br>    struct Suite2 {<br>        @Test<br>        func stuff() async throws {<br>            // Execute test code<br>        }<br>    }<br>}</pre><ul><li><strong>Avoid shared state.</strong> Make state modifications local to each test. If you must use shared resources, use <a href="https://developer.apple.com/documentation/swift/tasklocal">@TaskLocal</a> with <a href="https://developer.apple.com/documentation/testing/testscoping">Test Scoping</a>. This ensures each test have their own copy of mutable state. To use this you will have to migrate the code that accesses this state to Swift Structured Concurrency. You can migrate test suites one by one from the serialized suites created in above step.</li></ul><pre>class APICredentials {<br>    // Step-1: Apply @TaskLocal to static field<br>    @TaskLocal static var current = APICredentials()<br>}<br><br>// Step-2: Migrate code that uses the static field to Swift Concurrency<br><br>// Step-3: Create Test Scope for mocking shared state<br>struct MockAPICredentialsTrait: TestTrait, SuiteTrait, TestScoping {<br>    func provideScope(for test: Test, testCase: Test.Case?, performing function: @Sendable () async throws -&gt; Void) async throws {<br>        let mockCredentials = APICredentials(apiKey: &quot;...&quot;)<br>        try await APICredentials.$current.withValue(mockCredentials) {<br>            try await function()<br>        }<br>    }<br>}<br><br>// Step-3: Define tag name for the test scope usage<br>extension Trait where Self == MockAPICredentialsTrait {<br>    static var mockAPICredentials: Self {<br>        Self()<br>    }<br>}<br><br>// Step-4: Use the test scope as tag with tests and test suites<br>@Test(.mockAPICredentials)<br>func example() {<br>    // ...validate API usage, referencing `APICredentials.current`...<br>}</pre><h4>Challenge #3: Inheriting Suites Doesn’t Inherit Its Tests</h4><p>In XCTest, if you subclass a XCTestCase, the inherited test methods are automatically part of the subclassed XCTestCase. This is because test methods are defined as part of class and can be discovered easily with class inheritance hierarchy. In Swift Testing, even if you declare tests as methods in class, they are actually definitions generated by @Test macro, not tied to class definition in any actual sense:</p><pre>@available(*, deprecated, message: &quot;This function is an implementation detail of the testing library. Do not use it directly.&quot;)<br>@Sendable private static func $s22SwiftTestingUsageTests12CounterSuiteV8inrement4TestfMp_9Z181b5ab3fMu_() async throws -&gt; Void {<br>  let $s22SwiftTestingUsageTests12CounterSuiteV8inrement4TestfMp_7__localfMu_ = try await Testing.__requiringTry(Testing.__requiringAwait(CounterSuite()))<br>  _ = try await Testing.__requiringTry(Testing.__requiringAwait($s22SwiftTestingUsageTests12CounterSuiteV8inrement4TestfMp_7__localfMu_.inrement()))<br>}<br><br>@available(*, deprecated, message: &quot;This type is an implementation detail of the testing library. Do not use it directly.&quot;)<br>enum $s22SwiftTestingUsageTests12CounterSuiteV8inrement4TestfMp_38__🟠$test_container__function__181b5ab3fMu_: Testing.__TestContainer {<br>  static var __tests: [Testing.Test] {<br>    get async {<br>      return [<br>  .__function(<br>    named: &quot;inrement()&quot;,<br>    in: CounterSuite.self,<br>    xcTestCompatibleSelector: nil,<br>    displayName: &quot;Test increment&quot;,traits: [],sourceLocation: Testing.SourceLocation(fileID: &quot;SwiftTestingUsageTests/SwiftTestingUsageTests.swift&quot;, filePath: &quot;/Users/soumya.mahunt/Documents/moengage/pocs/swift-testing-usage/Tests/SwiftTestingUsageTests/SwiftTestingUsageTests.swift&quot;, line: 22, column: 6),<br>    parameters: [],<br>    testFunction: $s22SwiftTestingUsageTests12CounterSuiteV8inrement4TestfMp_9Z181b5ab3fMu_<br>  )<br>]<br>    }<br>  }<br>}</pre><p><strong>Why This Matters</strong></p><p>In your XCTest suite you might rely on test inheritance, to assert same behaviours with multiple configurations.</p><pre>class FeatureAWithConfigX: XCTestCase {<br>    override setup() {<br>        // setup config X<br>    }<br><br>    // Test behaviour<br>    func testStuff() {}<br>}<br><br>class FeatureAWithConfigY: FeatureAWithConfigX {<br>    override setup() {<br>        // setup config Y<br>    }<br><br>    // No need to write tests for same behaviours<br>}</pre><p>You could miss important tests during the migration.</p><pre>@Suite<br>class FeatureAWithConfigX {<br>    init() {<br>        // setup config X<br>    }<br><br>    // Test behaviour<br>    @Test<br>    func stuff() {}<br>}<br><br>@Suite<br>class FeatureAWithConfigY: FeatureAWithConfigX {<br>    override init() {<br>        // setup config Y<br>    }<br><br>    // Write the tests again?<br>}</pre><p><a href="https://github.com/swiftlang/swift-testing/issues/1185">Swift Testing as of now doesn’t support such test organization</a>, this may require restructuring your test hierarchy.</p><p><strong>Recommendation</strong></p><ul><li>If you have smaller number of tests and these tests can be migrated to <a href="https://developer.apple.com/documentation/testing/parameterizedtesting">Swift Parametrized Testing</a>, opt for that.</li><li>Otherwise currently, you can combine XCTests and Swift Testing in the same target. Wait for test inheritance to be available in Swift Testing before migrating these type of tests.</li></ul><h4>Wrapping Up: A Mindful Migration</h4><p>Migrating from XCTest to Swift Testing is not a mere mechanical process. It’s an opportunity to rethink your test design and embrace modern Swift paradigms like structured concurrency and immutability. However, it also comes with its own challenges, including the need to adapt to task contexts for assertions, deal with concurrency issues, missing test inheritance and absence of UI testing and performance testing APIs.</p><p>By keeping in mind the issues outlined here — and planning ahead — you can ensure a smoother migration. And remember, don’t just rely on the official migration guide. Your own experience and insights, coupled with community knowledge, will be your best tools.</p><p>What challenges have you faced while migrating to Swift Testing? Drop your thoughts in the comments below!</p><p>Happy testing! 🚀</p><h4>Bonus</h4><p><a href="https://gist.github.com/soumyamahunt/5493be57c69323b848e4b2528e39f67f#file-convert_tests-rb">Swift testing common conversion scenario script.</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b9c1d749ebd5" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Support Swift macros with CocoaPods]]></title>
            <link>https://soumyamahunt.medium.com/support-swift-macros-with-cocoapods-3911f9317042?source=rss-41d824adb39d------2</link>
            <guid isPermaLink="false">https://medium.com/p/3911f9317042</guid>
            <category><![CDATA[cocoapods]]></category>
            <category><![CDATA[swift-macros]]></category>
            <category><![CDATA[swift-programming]]></category>
            <category><![CDATA[macro]]></category>
            <category><![CDATA[swift]]></category>
            <dc:creator><![CDATA[Soumya Mahunt]]></dc:creator>
            <pubDate>Mon, 29 Jan 2024 16:41:53 GMT</pubDate>
            <atom:updated>2024-01-29T16:41:53.464Z</atom:updated>
            <content:encoded><![CDATA[<h4>Guide to distribute your macros using CocoaPods</h4><p>With the introduction of macros in Swift 5.9, removing common boilerplate from code has never been easier in Swift. However, the development of macros is more closely tied to Swift Package Manager, and depending on your use cases this might be a limitation to you, whether you are a macro library author losing out on CocoaPods users, or you are planning to introduce macro to an existing code base that hasn’t adopted SwiftPM.</p><p>In this article, we will discover how to create CocoaPods macro library from a Swift macro package and distribute it similar to any other CocoaPods library.</p><h4>Basics of SwiftPM Macro library</h4><p>Xcode 15 supports creating macro library with SwiftPM right out of the box with custom template Swift Package template:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cWEJGfgOUvusilbRowV3Tg.png" /></figure><p>When you create a a macro package, you will notice following things in your Package.swift manifest:</p><ul><li>A macro target containing macro implementation which uses <a href="https://github.com/apple/swift-syntax">swift-syntax</a> as dependency.</li><li>A library target containing macro definition and uses macro target as dependency.</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/3447e1e45afadd106a700e04bc87b520/href">https://medium.com/media/3447e1e45afadd106a700e04bc87b520/href</a></iframe><p>When going into macro target you will find a type that conforms to CompilerPlugin with all the macros provided inprovidingMacros property, and has the @main attribute attached:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/d10a611a83ebc4eeedfb564cffb9dad6/href">https://medium.com/media/d10a611a83ebc4eeedfb564cffb9dad6/href</a></iframe><p>You might have noticed this @main attribute before to declare application execution entry point. In fact, a macro is also an application/program, that generates some code output based on input. The only difference is that macros run on the host platform (i.e. macOS, the platform that builds application) not on the target platform (i.e. iOS, tvOS etc. the platform application will run on). This allows macros to not introduce additional overhead when user is running the application.</p><p>In the next section, we will see how Swift allows macro executables to be passed directly without SwiftPM usage.</p><h4>Macros outside SwiftPM</h4><p>Swift exposes two input options for macros to be injected directly from command line:</p><blockquote>-load-plugin-executable &lt;path&gt;#&lt;module-names&gt;<br> Path to an executable compiler plugins and providing module names such as macros<br>-load-plugin-library &lt;path&gt;<br> Path to a dynamic library containing compiler plugins such as macros</blockquote><p>From the previous section we know that macro is a simple program and now we can build it as an executable and provide executable path and macro module name to Swift in -load-plugin-executable to use the macro executable:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f29895edb61ea168467b829c9db04e1f/href">https://medium.com/media/f29895edb61ea168467b829c9db04e1f/href</a></iframe><p>Now that we have the macro executable ready, we can move to creating the podspec for our CocoaPods macro library.</p><h4>Create CocoaPods macro library</h4><p>Now we can create macro library podspec, that will provide the macro executable to Swift:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/d26280382f7801eeef6781894bb36438/href">https://medium.com/media/d26280382f7801eeef6781894bb36438/href</a></iframe><p>Few things to note here in the podspec:</p><ul><li>Macro definition files are added to <a href="https://guides.cocoapods.org/syntax/podspec.html#source_files">source_files</a>.</li><li>The path to macro executable is added to <a href="https://guides.cocoapods.org/syntax/podspec.html#preserve_paths">preserve_paths</a> so that CocoaPods doesn’t delete it after pod install.</li><li>The macro executable path and module name argument are provided to <a href="https://guides.cocoapods.org/syntax/podspec.html#user_target_xcconfig">user_target_xcconfig</a> as OTHER_SWIFT_FLAGS, <strong>the executable must be provided to user/app target not to our library target</strong>.</li></ul><p>While this will work just like other CocoaPods library, the major limitation of this approach is you need the macro executable beforehand. As a library author, this introduces additional challenge of building executable as part of <a href="https://en.wikipedia.org/wiki/Continuous_deployment">Continuous Deployment</a> workflow, choosing additional host for distributing with CocoaPods or to add the executable among source files itself (making the package cloning for SwiftPM slower). In the next section, we will see how we don’t need to have pre-built executable and distribute macro implementation source files just like we do in SwiftPM.</p><h4>Bringing CocoaPods macro library to SwiftPM level</h4><p>The only approach to use macro outside SwiftPM is to provide the macro product from command line, but we don’t have to build the macro product before hand, we can add the macro product building task as <a href="https://guides.cocoapods.org/syntax/podspec.html#script_phases">script_phase</a> and provide the built executable to Swift.</p><p>While you can use other tools like <a href="https://cmake.org/">cmake</a>, here we will use SwiftPM to build our macro executable using the same Package.swift manifest, and update the podspec with new details:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e5ddeef4ef327e035c84afc9c84354b0/href">https://medium.com/media/e5ddeef4ef327e035c84afc9c84354b0/href</a></iframe><p>Additional things to note in our new podspec:</p><ul><li>Package.swift manifest and macro source files are added to <a href="https://guides.cocoapods.org/syntax/podspec.html#preserve_paths">preserve_paths</a> instead to make sure these files don’t get deleted by CocoaPods after pod install.</li><li>Xcode build environment variables are not passed to Swift build task using command env -i PATH=”$PATH” “$SHELL” -l -c, to allow Swift building for our host platform not target platform.</li><li>In the Swift build task:<br>- Macro executable product MyMacroMacros is built with release configuration.<br>- Path to host machine SDK is provided with xcrun — show-sdk-path, as macro will be built to run on host machine.<br>- The path to Package.swift manifest directory is provided with — package-path option and the location of macro target build files provided with — scratch-path option.</li><li>In the <a href="https://guides.cocoapods.org/syntax/podspec.html#script_phases">script_phase</a>, Package.swift manifest and macro source files are provided as input_files and built macro executable is provided as output_files. This allows the build task to only run if there is change in source files or the executable product hasn’t been built yet, optimizing the build process.</li><li>The <a href="https://guides.cocoapods.org/syntax/podspec.html#script_phases">script_phase</a> runs before compiling allowing the macro executable to be provided to Swift build process.</li><li>The new macro executable path and module name argument are provided to <a href="https://guides.cocoapods.org/syntax/podspec.html#user_target_xcconfig">user_target_xcconfig</a> as OTHER_SWIFT_FLAGS, <strong>the executable must be provided to user target/app not to our library target</strong>.</li></ul><h4>Advantages over SwiftPM</h4><p>The subtle advantages of distributing macros with CocoaPods in this approach over SwiftPM are :</p><ul><li>The deployment target needs to be set higher to allow <a href="https://github.com/apple/swift-syntax">swift-syntax</a> usage, in case of SwiftPM. By separating macro definitions from the implementation, we can set lower deployment target for our macro library.</li><li>In case of SwiftPM, <a href="https://github.com/apple/swift-syntax">swift-syntax</a> version will be resolved to a common version, thereby introducing chances of version conflicts. While here, since macro implementation is separate, each CocoaPods macro library can use separate <a href="https://github.com/apple/swift-syntax">swift-syntax</a> version independently.</li></ul><h4>Conclusion</h4><p>By following these steps, now macro libraries can also be distributed as CocoaPods library, same as we can already distribute as Swift package. The library can be included by Xcode targets in the same way as any other CocoaPods library. For more concrete example, you can have a look at my Codable macro library <a href="https://github.com/SwiftyLab/MetaCodable">MetaCodable</a>‘s <a href="https://github.com/SwiftyLab/MetaCodable/blob/main/Package.swift">Package.swift</a> manifest and <a href="https://github.com/SwiftyLab/MetaCodable/blob/main/MetaCodableMacro.podspec">podspec</a>s.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3911f9317042" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Combining Interface Builder With JavaScript for Server-driven Apps]]></title>
            <link>https://medium.com/better-programming/combining-interface-builder-with-javascript-for-server-driven-apps-c1f907c1294?source=rss-41d824adb39d------2</link>
            <guid isPermaLink="false">https://medium.com/p/c1f907c1294</guid>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[server-side-rendering]]></category>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[ios-app-development]]></category>
            <dc:creator><![CDATA[Soumya Mahunt]]></dc:creator>
            <pubDate>Mon, 05 Jun 2023 16:27:13 GMT</pubDate>
            <atom:updated>2023-06-06T00:08:23.353Z</atom:updated>
            <content:encoded><![CDATA[<h4>Building dynamic UI with nibs and storyboards (Part 2)</h4><figure><img alt="Server driven app image" src="https://cdn-images-1.medium.com/max/750/1*SztIpuTg4uD6Pddh_I0xMg.png" /><figcaption><a href="https://www.freepik.com/vectors/cloud-application">Image from freepik</a></figcaption></figure><p>In the <a href="https://soumyamahunt.medium.com/superpower-of-interface-builder-no-one-talks-about-430c5ffcc6e1">first part</a> of this series, we saw building dynamic UI leveraging loose coupling of IB-built UIs. If you have not read it, go through it first. This article will build up on the major limitations it had. The main one cannot add or update controller logic code or event triggers/actions. This article will leverage JavaScript for dynamic logic and our dynamic UI from Interface Builder.</p><p>We will dive deeper into Objective-C runtime and advanced features like dynamic method resolution and message invocations while using Swift. We will also bridge types between JavaScript and Swift, establishing two-way communication between these runtimes.</p><h3>Why JavaScript?</h3><p>The goal we are trying to accomplish here is to enable dynamic logic populated by the remote server without updating the app. Due to iOS security limitations, it is not possible to add and load the random dynamic library after the app’s published to the store:</p><blockquote>To protect the system and other apps from loading third-party code inside their address space, the system performs a code signature validation of all the dynamic libraries that a process links against at launch time.</blockquote><p>This prevents us from using compiled languages like Swift. The workaround is to use interpreted languages like JavaScript and Python. Since iOS already has a JavaScript interpreter bundled with the system browser and with the help of <a href="https://developer.apple.com/documentation/javascriptcore">JavaScriptCore</a>, we can establish Swift-JavaScript communication easily. We are going to use JavaScript for this example.</p><h3>Bridging Swift-JavaScript</h3><p><a href="https://developer.apple.com/documentation/javascriptcore">JavaScriptCore</a> framework allows us to establish a two-way communication bridge between JavaScript and Swift, sharing data, invoking methods, and passing callbacks. We must create a JSContext to execute JavaScript code to achieve this communication. Since we want to allow UI modification from this context, we must create the <a href="https://developer.apple.com/documentation/javascriptcore/jsvirtualmachine">JSVirtualMachine</a> to host this context from the main thread. This will allow <a href="https://developer.apple.com/documentation/javascriptcore/jsvirtualmachine">JSVirtualMachine</a> to inherit the main thread’s run-loop for code execution.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/aaf4160c58505ce95eae25df019d864a/href">https://medium.com/media/aaf4160c58505ce95eae25df019d864a/href</a></iframe><p>We are passing the Swift controller instance to JavaScript to allow direct modification. Since we need to keep string reference of this context to ensure the lifetime is the same as the controller, the controller instance is passed as a weak reference wrapped inside a closure to avoid retaining the cycle.</p><p>Passing the object to JavaScript doesn’t allow access to all the instance methods or properties. We have to manually export them by confirming them to <a href="https://developer.apple.com/documentation/javascriptcore/jsexport">JSExport</a>. We must create an export protocol that inherits <a href="https://developer.apple.com/documentation/javascriptcore/jsexport">JSExport</a> for each class and add the desired properties and methods to export. For this example, we will expose the IBOutlets of the controller and text property for UILabel.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/a077b49bf19ae408b6acdb5bfac0c113/href">https://medium.com/media/a077b49bf19ae408b6acdb5bfac0c113/href</a></iframe><p>Now, we are ready to consume JavaScript code from the server and execute it in created context. The file will be provided with the same approach as the nib files used by the server in the <a href="https://soumyamahunt.medium.com/superpower-of-interface-builder-no-one-talks-about-430c5ffcc6e1">previous article</a>. We can extract the content of the downloaded file and execute it in our context.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/1498e96f308289670dd13489618ad6b5/href">https://medium.com/media/1498e96f308289670dd13489618ad6b5/href</a></iframe><p>Now that we have the JS bridge set up, in the next sections, we will discuss how to expose all the IBOutlets dynamically and forward action events to JavaScript.</p><h3>Dynamic IBOutlets</h3><p>IBOutlets are used to keep a reference of UI objects to modify their behaviour later, i.e., changing text, colour, etc. Typically, one IBOutlet can be attached to one property, or multiple IBOutlets can be attached as a group to an array property. In this section, we will store outlets as key-value pairs, with the key being the property name defined in xib or storyboard.</p><p>Adding @IBOutlet to the property field achieves the following things:</p><ul><li>Expose the field name to IB to give feedback in Xcode on whether the outlet is attached.</li><li>In the case of Swift, the property is exposed to Obj-C similar to @objc attribute, and makes the property accessible via <a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueCoding/index.html">key-value coding</a>.</li></ul><p>During the initialization of the controller from nib, all such outlets are assigned with <a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueCoding/index.html">key-value coding</a> using <a href="https://developer.apple.com/documentation/objectivec/nsobject/1415969-setvalue">setValue(_:forKey:)</a>, where the UI object is passed as value and the outlet field name as the key. In our case, since the field doesn’t exist, <a href="https://developer.apple.com/documentation/objectivec/nsobject/1413490-setvalue">setValue(_:forUndefinedKey:)</a> is invoked, which by default raises an exception.</p><blockquote>Terminating app due to uncaught exception ‘NSUnknownKeyException’, reason: ‘[&lt;{controllerClass} {address}&gt; setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key {outletName}.’</blockquote><p>where controllerClass is the controller class name, address represents memory address, and, most importantly, outletName represents the outlet that is not attached. This exception is being raised since we don’t have any property with the outlet name exposed.</p><p>To avoid exceptions and accomplish our task, we can override this method to store the objects in our own dictionary. Similarly, we have to override <a href="https://developer.apple.com/documentation/objectivec/nsobject/1413457-value">value(forUndefinedKey:)</a> to allow getting the properties from our custom dictionary.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/274f00617b1545519e2637b032853a99/href">https://medium.com/media/274f00617b1545519e2637b032853a99/href</a></iframe><p>Now that we have exposed outlets to JavaScript, we can set exported properties from JavaScript code. Let’s see the label’s text live update after attaching Safari Web Inspector to our <a href="https://developer.apple.com/documentation/javascriptcore/jscontext">JSContext</a> and setting the label’s text.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UxrtV7fSr6yI9GHSFTA2Kw.gif" /></figure><h3>Dynamic IBActions</h3><p>Similar to IBOutlets, IBActions are just Obj-C methods that can be invoked via <a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtHowMessagingWorks.html">messaging</a>. When the associated event is triggered, the message is passed with the attributes specified. The absence of methods causes exceptions on the event trigger.</p><blockquote>Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘-[{controllerClass} {actionSelector}]: unrecognized selector sent to instance {address}’</blockquote><p>To resolve this for our use case, we can forward the method invocation to the JavaScript object that contains the implementation. There are multiple ways of adding message forwarding in Obj-C:</p><ul><li><a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtDynamicResolution.html">Dynamic method resolution</a>: After getting a call back in <a href="https://developer.apple.com/documentation/objectivec/nsobject/1418500-resolveinstancemethod">resolveInstanceMethod(_:)</a> for an unimplemented method <a href="https://developer.apple.com/documentation/objectivec/1418901-class_addmethod">class_addMethod(_:_:_:_:)</a> can be used to provide a dynamic implementation.</li><li><a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtForwarding.html">Message Forwarding</a>: Messages related to unimplemented methods can be forwarded to another object that handles the message. Forwarding has two approaches:<br>- Using <a href="https://developer.apple.com/documentation/objectivec/nsobject/1571955-forwardinvocation">forwardInvocation:</a> which is only available in Obj-C.<br>- Using the less expensive <a href="https://developer.apple.com/documentation/objectivec/nsobject/1418855-forwardingtarget">forwardingTarget(for:)</a> available in Swift.</li></ul><p>In this example, we will use the first approach, supporting up to two arguments. Since the direct conversion of Swift variadic methods to C variadic methods is impossible, we must implement multiple methods based on the arguments count approach. The advantage of <a href="https://developer.apple.com/documentation/objectivec/nsobject/1418500-resolveinstancemethod">resolveInstanceMethod(_:)</a> is it is invoked once for a selector and class type combination. Subsequent message passing will pick up the selector implemented in the first instance.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/ba25eb8827c9b77131aa6b5abb961b52/href">https://medium.com/media/ba25eb8827c9b77131aa6b5abb961b52/href</a></iframe><p>Note that <a href="https://developer.apple.com/documentation/objectivec/nsobject/1418500-resolveinstancemethod">resolveInstanceMethod(_:)</a> will also be invoked for dynamic IBOutlets, but since we already have a specific implementation in place for them, we can only provide an implementation for IBActions. In the snippet provided above, we are detecting selectors beginning with action as our dynamic IBActions.</p><p>With the client implementation complete, we can create our JavaScript file that initializes our dynamic page with some data, implements dynamic IBActions that handle the callback from our dynamic view, and uses dynamic IBOutlets to update data in the dynamic page.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/0644998cac4c0160c3c4116d29ecd30a/href">https://medium.com/media/0644998cac4c0160c3c4116d29ecd30a/href</a></iframe><h4>Let’s see our work in action</h4><p>Our sample app with a simple counter is now ready and completely functional, with all the user actions being reflected on UI timely and properly. The beauty of this is our dynamic UI is completely native, and our controller logic is completely dynamic. Finally, we have achieved a completely server-driven UI and server-driven state and user-action management.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LtBE0MhfuoL_YGMFH2kuiw.gif" /></figure><p>Now we can modify our controller logic and page UI by modifying their corresponding js and xib file, respectively. After rerunning our server to publish the latest changes, we can refresh our implemented dynamic page (by trying to land on the page again) to see our latest UI changes and user interaction effect without redeploying or restarting our iOS client app.</p><h3>Conclusion</h3><p>Although this example built is pretty simple, this can be extended to build a fully functional app.</p><ul><li>Using caching for better page load performance</li><li>Using versioning to tie nib/storyboard files with their js counterpart and avoid runtime exceptions possible due to IBActions mismatch.</li><li>Using native code that can be populated with data from the JavaScript layer to perform complex UI modifications, i.e., animations, etc.</li></ul><p>This, in principle, works like a website while behaving like a native app. You can build a complete server-driven app solution with a completely native UI and customisable state, user-action, and routing management.</p><p><em>The one side effect of using this approach is that since view-controllers are being passed to JavaScript, JavaScript memory management rules also apply to them. The controller will be deallocated once JavaScript’s GC releases its reference. Unlike Swift’s ARC uses a </em><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_management#mark-and-sweep_algorithm"><em>different GC algorithm</em></a><em> which releases memory periodically, and hence there will be some delay in </em><em>deinit invocation after popping back from a controller.</em></p><p>Let me know if you are using any other approaches for your server-driven app. Also, let me know if you see any drawbacks to this approach. The complete implementation can be found at:</p><p><a href="https://github.com/SwiftyLab/DynamicNib">GitHub - SwiftyLab/DynamicNib: Dynamically load nibs in ios app</a></p><p>The following are the resources used for this article. You can delve deeper into these for more details:</p><ul><li><a href="https://support.apple.com/en-in/guide/security/sec7c917bf14/web">App code signing process in iOS and iPadOS</a></li><li><a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueCoding/index.html">About Key-Value Coding</a></li><li><a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtHowMessagingWorks.html">Messaging</a></li><li><a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtDynamicResolution.html">Dynamic Method Resolution</a></li><li><a href="https://developer.apple.com/documentation/javascriptcore">JavaScriptCore | Apple Developer Documentation</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_management">Memory management - JavaScript | MDN</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c1f907c1294" width="1" height="1" alt=""><hr><p><a href="https://medium.com/better-programming/combining-interface-builder-with-javascript-for-server-driven-apps-c1f907c1294">Combining Interface Builder With JavaScript for Server-driven Apps</a> was originally published in <a href="https://betterprogramming.pub">Better Programming</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Superpower of Interface Builder no one talks about]]></title>
            <link>https://itnext.io/superpower-of-interface-builder-no-one-talks-about-430c5ffcc6e1?source=rss-41d824adb39d------2</link>
            <guid isPermaLink="false">https://medium.com/p/430c5ffcc6e1</guid>
            <category><![CDATA[xcode]]></category>
            <category><![CDATA[ios-development]]></category>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[interface-builder]]></category>
            <category><![CDATA[uikit]]></category>
            <dc:creator><![CDATA[Soumya Mahunt]]></dc:creator>
            <pubDate>Mon, 29 May 2023 14:09:01 GMT</pubDate>
            <atom:updated>2023-05-31T07:10:53.797Z</atom:updated>
            <content:encoded><![CDATA[<h4>Building dynamic UI with nibs and storyboards (Part 1)</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/900/1*RoG7N9BvOoU0ae-kKi_5GQ.png" /></figure><p>There is huge debate among iOS developers regarding different approaches of building application user interface, the approaches being categorized as: <strong>Interface Builder</strong> (<strong>Storyboards/XIBs/NIBs)</strong>, <strong>Code (Custom/SwiftUI) </strong>with each having their pros and cons that this article won’t go deep into. With the advent of SwiftUI building UI with code has become lot easier while allowing a lot more reusability. I read lots of posts comparing all these approaches, and none mentioned this particular advantage of building UI with <strong>Interface Builder</strong> due to the clear separation of UI from logic.</p><p>Typically when adding dynamic UI that needs to be consistent across app versions and can be updated without updating the app itself, UI is built with web frameworks while displayed using web-view. Similarly, while building third party framework or SDKs i.e. payments SDK, consumers can customize the UI without having access to data or state. In this article, we will see this use case being fulfilled by using completely native UI stack with the help of <strong>Interface Builder</strong>.</p><h4>Interface Builder mechanics</h4><p>Before diving into the use case and the advantage of Interface Builder, let’s first understand how IB works. The visual diagram built with IB is stored in XML format in <strong>xib</strong> or <strong>storyboard</strong> files. In fact, <strong>XIB</strong> stands for <strong>XML Interface Builder</strong> which is the oldest file format for IB. During build phase, the <strong>xib</strong> and <strong>storyboard</strong> files are compiled into <strong>nib</strong> and <strong>storyboardc </strong>by<strong> ibtool </strong>and copied inside the app directory or a specified bundle.</p><p>These files can then be loaded into memory by using <a href="https://developer.apple.com/documentation/uikit/uinib/1614138-init">UINib</a> and <a href="https://developer.apple.com/documentation/uikit/uistoryboard/1616216-init">UIStoryboard</a> initializer or <a href="https://developer.apple.com/documentation/uikit/uiviewcontroller/1621359-init">UIViewController</a> initializer for view controllers defined in a XIB, and subsequently view objects can be created. Due to this separation of UI in a separate file, building UI with code performs better as no file load is needed. But this kind of separation has the following benefits:</p><ul><li>Smaller update package when only xibs or storyboards are modified in newer version, i.e. only the nib or storyboardc files are updated.</li><li>Sharing same view object code across multiple nibs, i.e. using the same UITableViewCell sub-class for multiple cell nibs.</li><li><strong>Independently update UI of the app without modifying underlying control logic.</strong></li></ul><p>While updating UI of the app dynamically without pushing app updates can be done in building UI with code approaches as well, but that requires establishing custom serialization and deserialization framework that takes some data from remote server to build dynamic UI. But with IB the unarchiving part is already provided by iOS, we just have to take advantage of it after implementing fetching data from remote server. In the subsequent part, we will implement a POC showcasing this use case in action.</p><h4>Providing dynamic UI from server</h4><p>To test the length of loosely coupling of UI built with IB and controlling logic, we will not add the nib files in the distributed app. Rather we will build a sample server that provides all the nib files. We will use <a href="https://github.com/vapor/vapor">vapor</a> to build our server in Swift, that provides requested nib files to our iOS client.</p><p>Since our xib files aren’t included in an iOS product, they won’t be compiled to nib automatically by Xcode. But we can write a <a href="https://github.com/apple/swift-package-manager/blob/main/Documentation/Plugins.md#build-tool-plugins">build tool Swift package plugin</a> that compiles these files to nib by using ibtool as part of our server build process:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/fd2aae85667ded3a2d3d3468f1579361/href">https://medium.com/media/fd2aae85667ded3a2d3d3468f1579361/href</a></iframe><p>Now that in the nib files are generated as part of our build process and copied to server app’s bundle, we can provide these file to our iOS clients depending on the file name specified in the request made by client:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/a5ac4e47be57952083266956c43821f2/href">https://medium.com/media/a5ac4e47be57952083266956c43821f2/href</a></iframe><h4>Building client with dynamic UI</h4><p>With the changes our server is now ready to provide nib files to our iOS client, now, we just have to implement downloading these files and loading into memory to unarchive UI objects. But just downloading the file isn’t enough as to load file in the memory we need to specify the bundle that provides the file. If we look at any bundle, it is just a directory ending in .bundle. For our use case, we can just create such directory and store our IB files:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/b5a4b1c7732e7f3f3535fe989cb96990/href">https://medium.com/media/b5a4b1c7732e7f3f3535fe989cb96990/href</a></iframe><p>Now, with that out of the way, we can implement downloading the nib files and storing inside our newly created bundle directory. We can pass the file name and bundle instance to controller initializer, and display the dynamic page in our app.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/0d48b2ee135b44dd111565c4ca072dfc/href">https://medium.com/media/0d48b2ee135b44dd111565c4ca072dfc/href</a></iframe><p>We can use the same approach to register and display dynamic table view cells in our app as well. For tableview cells, we have to take extra care to not invoke the cell display logic in case the nib file isn’t downloaded already, doing so will prevent runtime exceptions.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e1a1b2d1b69668e4f23d1a31a6b09db4/href">https://medium.com/media/e1a1b2d1b69668e4f23d1a31a6b09db4/href</a></iframe><h4>The moment of truth</h4><p>Now we can modify our controller and cell UI by modifying their corresponding xib file and rerunning our server to publish the latest changes. We can refresh our implemented dynamic page (by trying to land on the page again) to see our latest UI changes without having to redeploy or restart our iOS client app.</p><figure><img alt="Dynamic UI demo with page color change and tableview cell text position change" src="https://cdn-images-1.medium.com/max/1024/1*3hN9WNmrYfJo6byxdxoMDQ.gif" /></figure><p>One caveat with this is, as long as all the <strong>IBOutlet</strong>s and <strong>IBAction</strong>s are intact in the xib or storyboard file, the controller logic will work without any impact. Otherwise there is possibility of runtime exceptions with this approach. Additionally, in controller logic some <strong>IBOutlet</strong>s can be declared optional to allow removal of such elements without causing exceptions in client iOS app.</p><h4>Conclusion</h4><p>Although the UI change shown here is only for xib file and pretty simple, this can be extended to storyboard file and complete UI revamp can be done by keeping <strong>IBOutlet</strong>s and <strong>IBAction</strong>s with the controller logic. Additionally, caching can be introduced for better page load performance and versioning of dynamic nibs can be implemented to link with controller logic present with multiple client app versions, preventing exceptions with missing <strong>IBOutlet</strong>s and <strong>IBAction</strong>s.</p><p>Let me know if you are using this approach in your app or are planning to build something like this. Also, if you are using any different approach for such use cases, let me know what drawback you foresee with this approach. The complete implementation can be found at:</p><p><a href="https://github.com/SwiftyLab/DynamicNib">GitHub - SwiftyLab/DynamicNib: Dynamically load nibs in ios app</a></p><p>Following are the resources used for this article, you can delve deeper into these for more customized use cases:</p><ul><li><a href="https://codewithchris.com/xcode-using-storyboards-and-xibs-versus-creating-views-programmatically/">XCode: Using Storyboards and Xibs Versus Creating Views Programmatically</a></li><li><a href="https://developer.apple.com/videos/play/wwdc2022/110401/">Create Swift Package plugins - WWDC22 - Videos - Apple Developer</a></li><li><a href="https://docs.vapor.codes/">Vapor Docs</a></li></ul><p><em>In the next part of this series, we will discuss making controller logic dynamic as well allowing to circumvent </em><strong><em>IBOutlet</em></strong><em>s and </em><strong><em>IBAction</em></strong><em>s limitations, allowing us to onboard completely new user journeys without any app update.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=430c5ffcc6e1" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/superpower-of-interface-builder-no-one-talks-about-430c5ffcc6e1">Superpower of Interface Builder no one talks about</a> was originally published in <a href="https://itnext.io">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to manage unstructured tasks with Swift’s structured concurrency]]></title>
            <link>https://itnext.io/how-to-manage-unstructured-tasks-with-swifts-structured-concurrency-6cc4329b4d13?source=rss-41d824adb39d------2</link>
            <guid isPermaLink="false">https://medium.com/p/6cc4329b4d13</guid>
            <category><![CDATA[concurrency]]></category>
            <category><![CDATA[tasks]]></category>
            <category><![CDATA[structured-programming]]></category>
            <category><![CDATA[structured-concurrency]]></category>
            <category><![CDATA[swift]]></category>
            <dc:creator><![CDATA[Soumya Mahunt]]></dc:creator>
            <pubDate>Mon, 20 Mar 2023 13:50:54 GMT</pubDate>
            <atom:updated>2023-05-30T04:47:17.615Z</atom:updated>
            <content:encoded><![CDATA[<h4>Swift structured concurrency shortcomings (Part 1)</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WZoCytm6QYW7Wx0HP0gZSA.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/@joshredd">Josh Redd</a> on <a href="https://unsplash.com/">Unsplash</a></figcaption></figure><p>Swift Structured Concurrency is a relatively new feature introduced with Swift 5.5. It is an innovative approach to managing asynchronous tasks that prevents task leaks, ensures cooperative cancellation, and makes the control flow more reasonable in a concurrent context rather than the earlier approach of passing callbacks. While this idea is constantly evolving with new features, one feature that is sorely missing is the simpler management (i.e cancellation) of unstructured tasks.</p><p>In this post, we will explore managing unstructured tasks in Swift Structured Concurrency and handling cancellation for unstructured tasks. We will look into other languages’ approaches and how we can do something similar in Swift.</p><h4>Why do we need unstructured tasks?</h4><p>Swift provides ways of creating structured tasks with async let keyword and <a href="https://developer.apple.com/documentation/swift/taskgroup">TaskGroup</a> which simplifies error propagation, cooperative cancellation, and execution from an asynchronous context by managing the hierarchy of tasks. These approaches should be preferred to creating unstructured tasks. However, for some scenarios, where there is no clear hierarchy of tasks, it is absolutely necessary to use unstructured tasks:</p><ul><li>Launching tasks from non-async code, i.e. fetching content from a server to display on UI.</li><li>Managing the lifetime of tasks based on some events, i.e terminating some account-specific async work when the user logs out of the account.</li></ul><p>Unstructured tasks can be created by using <a href="https://developer.apple.com/documentation/swift/task/">Task</a> APIs, with an option to whether created task inherits actor context or not. To manage the cancellation of such tasks, we have to store the task instance and invoke the <a href="https://developer.apple.com/documentation/swift/task/cancel()">cancel</a> method when such a need arises. But in such cases, we also have to take an accounting not invoking <a href="https://developer.apple.com/documentation/swift/task/cancel()">cancel</a> method for tasks that are completed successfully.</p><h4>Exploring other structured concurrency runtimes</h4><p>Kotlin has the concept of <a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/">CoroutineScope</a> using which newly spawned coroutines/asynchronous work can be canceled by canceling the scope. While <a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/">CoroutineScope</a> has other functionalities as well we will only consider the cancellation mechanism in this post:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/c6798419ae8c87e78bbbdfd384a361fc/href">https://medium.com/media/c6798419ae8c87e78bbbdfd384a361fc/href</a></iframe><p>.NET takes this cancellation mechanism one step further with <a href="https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken">CancellationToken</a> and <a href="https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtokensource">CancellationTokenSource</a> by allowing linking external <a href="https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken">CancellationToken</a> with <a href="https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtokensource">CancellationTokenSource</a>. When canceling a single source instance all the linked instances are also canceled:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/3062930ac3ee73e592eb53a90ddf75ae/href">https://medium.com/media/3062930ac3ee73e592eb53a90ddf75ae/href</a></iframe><h4>Implementing unstructured tasks cancellation handling</h4><p>While Swift doesn’t have any such built-in construct, we can create our own using the built-in cooperative cancellation mechanism. Using built-in <a href="https://developer.apple.com/documentation/swift/taskgroup">TaskGroup</a> we can register tasks to be canceled with the built-in cancellation handler:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/dac79fa2389a3a057462cc13143ce176/href">https://medium.com/media/dac79fa2389a3a057462cc13143ce176/href</a></iframe><p>By using this mechanism we can cancel all the tasks that aren’t completed by canceling the group. Some caveats we have to take into account:</p><ul><li><a href="https://developer.apple.com/documentation/swift/taskgroup">TaskGroup</a> can’t be used outside of tasks where it was created, hence we have to use <a href="https://developer.apple.com/documentation/swift/asyncstream">AsyncStream</a> for submitting tasks to <a href="https://developer.apple.com/documentation/swift/taskgroup">TaskGroup</a>. And since <a href="https://developer.apple.com/documentation/swift/task/">Task</a> is a generic type, our <a href="https://developer.apple.com/documentation/swift/asyncstream">AsyncStream</a>’s element type should be an existential type to handle all variations:</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/85c3022b0af862e385c0aae9235bc6aa/href">https://medium.com/media/85c3022b0af862e385c0aae9235bc6aa/href</a></iframe><ul><li>Awaiting <a href="https://developer.apple.com/documentation/swift/task/">Task</a>&#39;s result increases the priority of the task to the caller task’s priority. Hence we have to use the least priority for our cancellation task to avoid this side effect.</li></ul><p>Keeping these in mind, the implementation of our CancellationSource looks something like this:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/ded30863aa5ee771636e76be790a5273/href">https://medium.com/media/ded30863aa5ee771636e76be790a5273/href</a></iframe><p>We are using <a href="https://developer.apple.com/documentation/swift/asyncstream">AsyncStream</a>&#39;s continuation to register tasks for cancellation and terminating the stream in the event of cancellation. Note that, tasks are canceled instantly if submitted after cancellation has already been triggered.</p><p>We can even go one step further and allow registration of CancellationSources to be canceled. We can implement Cancellable protocol for CancellationSource with the help of waiting for the cancellation task’s result:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/0b74e2b6e7f166fd4b7e29d9c30e649e/href">https://medium.com/media/0b74e2b6e7f166fd4b7e29d9c30e649e/href</a></iframe><p>We can apply this implementation in an UIKit controller instance. We can keep two CancellationSource one that is canceled when the instance is deallocated and another canceled when the controller goes out of the user’s view. By keeping two cancellation sources, we can reason about whether an asynchronous work should be performed as long as the instance is alive on memory i.e. updating the view based on the app’s central state, or whether asynchronous work only needs to be performed when the controller instance is on user’s view i.e. fetching some data asynchronously to display user.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f765f3b2bb7211daf09301626dbd6b9b/href">https://medium.com/media/f765f3b2bb7211daf09301626dbd6b9b/href</a></iframe><h4>Further improvements</h4><p>There are certain limitations to this type of cancellation handling:</p><ul><li>When linking multiple CancellationSource there might be a possibility of circular linking. In such cases, the cancellation task will leak and never finish. This can be handled by only allowing linking during the initialization of a CancellationSourceand preventing registration after initialization.</li><li><a href="https://developer.apple.com/documentation/swift/taskgroup">TaskGroup</a> stores results of submitted tasks, and since these results are not consumed by the cancellation task, long usage of CancellationSource might leak child tasks. Fortunately by using upcoming <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0381-task-group-discard-results.md">DiscardingTaskGroup</a> such leaks could be prevented.</li></ul><h4>Conclusion</h4><p>Swift Structured Concurrency is evolving with every new version of Swift with the goal of being feature complete by Swift 6. With this article, I want to start a series of posts filling the feature gaps with other concurrency runtimes during the initial adoption phases. Managing unstructured tasks is an essential aspect of Swift Structured Concurrency, and developers should follow best practices to ensure that they manage unstructured tasks effectively. Let me know in the comments what other methods you are using to manage unstructured tasks and any other feature gaps you perceive in the new concurrency features.</p><p>The final implementation along with other primitives that might ease adoption are available below.</p><ul><li><a href="https://github.com/SwiftyLab/AsyncObjects/blob/main/Sources/AsyncObjects/CancellationSource/CancellationSource.swift">AsyncObjects/CancellationSource.swift at main · SwiftyLab/AsyncObjects</a></li><li><a href="https://github.com/SwiftyLab/AsyncObjects">GitHub - SwiftyLab/AsyncObjects: Several synchronization primitives and task synchronization mechanisms introduced to aid in modern swift concurrency.</a></li></ul><h4>Further reading</h4><ul><li><a href="https://developer.apple.com/videos/play/wwdc2021/10134/">Explore structured concurrency in Swift - WWDC21 - Videos - Apple Developer</a></li><li><a href="https://developer.apple.com/videos/play/wwdc2021/10254">Swift concurrency: Behind the scenes - WWDC21 - Videos - Apple Developer</a></li><li><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0381-task-group-discard-results.md">swift-evolution/0381-task-group-discard-results.md at main · apple/swift-evolution</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6cc4329b4d13" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/how-to-manage-unstructured-tasks-with-swifts-structured-concurrency-6cc4329b4d13">How to manage unstructured tasks with Swift’s structured concurrency</a> was originally published in <a href="https://itnext.io">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Decoding Dynamic JSON with Swift Codable]]></title>
            <link>https://itnext.io/decoding-dynamic-json-with-swift-codable-64160d06b456?source=rss-41d824adb39d------2</link>
            <guid isPermaLink="false">https://medium.com/p/64160d06b456</guid>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[json]]></category>
            <category><![CDATA[json-parsing]]></category>
            <category><![CDATA[polymorphism]]></category>
            <category><![CDATA[codable]]></category>
            <dc:creator><![CDATA[Soumya Mahunt]]></dc:creator>
            <pubDate>Wed, 06 Jul 2022 15:14:34 GMT</pubDate>
            <atom:updated>2023-03-18T08:37:13.150Z</atom:updated>
            <content:encoded><![CDATA[<h4>Handle dynamic server responses working with Swift’s sound type system</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1baFkR1wWxMue812JWuekQ.png" /></figure><p>Providing dynamic content in your app requires communicating with an external service to fetch data. The most common format that is used nowadays is JSON and the common way of handling JSON data in swift is to convert it to swift types with help of <a href="https://developer.apple.com/documentation/swift/codable">Codable</a>. In the majority of the use cases, the data can be represented with a concrete swift type having a static list of properties.</p><p>In other cases, when the content in your app is highly dynamic with a wide range of scope, the response data may vary between a wide range of formats. Some common examples are:</p><ul><li>Building dynamic server-driven UI with <a href="https://en.wikipedia.org/wiki/Content_management_system">CMS</a>.</li><li>Display a wide range of products on an e-commerce website.</li><li>Dynamic content feed in a social network.</li></ul><p>In this post, we will see the typical way of handling decoding these kinds of responses, and how <a href="https://github.com/SwiftyLab/DynamicCodableKit">DynamicCodableKit</a> makes it seamless to handle dynamic responses by building on top of Swift’s <a href="https://developer.apple.com/documentation/swift/codable">Codable</a>.</p><h3>Current State</h3><p>Following is Amazon’s suggestion response data when a user types any text in the search box, in this scenario “<strong>microwave”</strong>:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/8c90705d589d02981f2c7375fcfdbe5a/href">https://medium.com/media/8c90705d589d02981f2c7375fcfdbe5a/href</a></iframe><p>The suggestions property contains a list of dynamic results for the search, with each result containing a field type representing the actual type of result. In the above response, KEYWORD and WIDGET type results are received, with each type containing some additional metadata, which in turn is used to present the additional option to users like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/792/1*JisPLGXlF2ubv5pKoTcKlw.png" /></figure><p>One approach is to use a single struct with optional fields. Although this approach seems simpler, with this approach you can use default synthesized Codable implementation. The drawback of this approach is you have to pollute your business logic with data validation code and for cases where the same property can have a different type you are out of luck with this approach.</p><p>Another and much-preferred approach is to use an enum with associated values. It addresses the shortcoming of the previous approach of removing nil property requirements and solving property type conflicts by adding separate cases for each type to decode. Custom init(from:) implementation for the enum can be provided that will decode the underlying case/type.</p><p>In our example, we can check the type field for each object and decode the suggestion accordingly:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/dc1bd14abe85a87fd9f51a805327e5d1/href">https://medium.com/media/dc1bd14abe85a87fd9f51a805327e5d1/href</a></iframe><h3>Using DynamicCodableKit</h3><p>While the above approach works for responses with a smaller level of dynamism, it becomes much harder to maintain all this boilerplate. The major drawback is you have to do some additional work to access common properties across all cases and with enums, you have to update all the switch cases each time you add a new type.</p><p>This is where <a href="https://github.com/SwiftyLab/DynamicCodableKit">DynamicCodableKit</a> shines by using generic property wrappers instead. It handles dynamic decoding based on DynamicDecodingContext which is a <a href="https://fabernovel.github.io/2020-06-03/approaches-to-type-erasure-in-swift#:~:text=%20Approaches%20to%20Type%20Erasure%20in%20Swift%20,are%20very%20constrained%20to%20work%20with.%20More%20">type erasure</a> that decodes a specific DynamicDecodable type and casts it to its generic type. You can customize DynamicDecodingContext in different scenarios to dynamically decode multiple types. <a href="https://github.com/SwiftyLab/DynamicCodableKit">DynamicCodableKit</a> also provides <a href="https://swiftylab.github.io/DynamicCodableKit/documentation/dynamiccodablekit/collectiondecoding/">multiple configurations</a> for collection decoding, to customize what happens when invalid data is encountered, i.e. you can choose to decode only valid data while ignoring invalid data instead of just throwing an error.</p><p>For our current example, we can create a Suggestion protocol type that will provide access to common properties and methods for underlying actual Suggestions, i.e. KeywordSuggestion, WidgetSuggestion. SuggestionType and SuggestionTypeCodingKey can implement DynamicDecodingContextIdentifierKey and DynamicDecodingContextIdentifierCodingKey respectively to decode the actual types. Compared to the previous approach, adding new types only requires changing SuggestionType&#39;s associatedContext implementation:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/be8cd5380808afcd09c3fe2f0b1eed89/href">https://medium.com/media/be8cd5380808afcd09c3fe2f0b1eed89/href</a></iframe><blockquote>NOTE: By default, <em>DynamicDecodable</em> only supports down casting, if you want to cast to an unrelated type, you have to provide a custom implementation.</blockquote><h3>Conclusion</h3><p>Besides the above example, <a href="https://github.com/SwiftyLab/DynamicCodableKit">DynamicCodableKit</a> provides several property wrappers and associated protocols to handle various dynamic decoding scenarios, all of them explained in detail in the <a href="https://swiftylab.github.io/DynamicCodableKit/documentation/dynamiccodablekit/">documentation</a> making dynamic decoding a lot simpler to use with minimal boiler-plate.</p><p><a href="https://github.com/SwiftyLab/DynamicCodableKit">GitHub - SwiftyLab/DynamicCodableKit: Implement dynamic JSON decoding within the constraints of Swift&#39;s sound type system by working on top of Swift&#39;s Codable implementations.</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=64160d06b456" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/decoding-dynamic-json-with-swift-codable-64160d06b456">Decoding Dynamic JSON with Swift Codable</a> was originally published in <a href="https://itnext.io">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>