<?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 Kamil Wyszomierski on Medium]]></title>
        <description><![CDATA[Stories by Kamil Wyszomierski on Medium]]></description>
        <link>https://medium.com/@kamil.wyszomierski?source=rss-c372b063c70e------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*FJPME_h4VldueKvNd3HBJA.jpeg</url>
            <title>Stories by Kamil Wyszomierski on Medium</title>
            <link>https://medium.com/@kamil.wyszomierski?source=rss-c372b063c70e------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 24 Apr 2026 11:51:09 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@kamil.wyszomierski/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[Automate versioning for Swift Packages]]></title>
            <link>https://medium.com/@kamil.wyszomierski/automate-versioning-for-swift-packages-6bcba80e59f7?source=rss-c372b063c70e------2</link>
            <guid isPermaLink="false">https://medium.com/p/6bcba80e59f7</guid>
            <category><![CDATA[automated-version-control]]></category>
            <category><![CDATA[swift-package-manager]]></category>
            <category><![CDATA[version-control]]></category>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[automation]]></category>
            <dc:creator><![CDATA[Kamil Wyszomierski]]></dc:creator>
            <pubDate>Fri, 20 Jun 2025 20:30:36 GMT</pubDate>
            <atom:updated>2025-06-20T20:30:36.540Z</atom:updated>
            <content:encoded><![CDATA[<h3>Versioning automation for Swift Packages</h3><figure><img alt="Three bikes, each captioned with version, in order starting from an oldest prototyped from eighteenth century, followed by bicycle from 1870, ending on newest one, used in modern days." src="https://cdn-images-1.medium.com/max/1024/1*bVgV77S6ma7GFTBYa9JBDA.png" /><figcaption><a href="https://github.com/Filozoff/BlogArticles/blob/master/Article003/Resources/swift_package_versioning.png">Image created by author using Procreate</a></figcaption></figure><p><em>(This article assumes you have basic knowledge of shell scripts)</em></p><h3>The challenge of version management</h3><p>Shared library version management can be a daunting task. Whether you’re working on an in-house project or maintaining a public package, keeping track of changes and their impact becomes increasingly complex as the project grows. Even for solo developers, there’s a significant risk of introducing breaking changes without properly marking them with a major version bump.</p><h3>The solution: automated versioning</h3><p>This article presents a practical solution through automation. We’ll explore how to either fully automate the versioning process or verify manually proposed versions, ensuring consistent and correct semantic versioning across your Swift packages.</p><h3>Understanding Swift Package Manager versioning</h3><p>Swift Package Manager (SPM) uses <a href="https://semver.org">Semantic Versioning</a> (semver). While Semantic Versioning supports suffixes (like 1.0.1-alpha), SPM works best with the &quot;version core&quot; only (e.g., 1.0.1). Tagging changes with just the version core enables library consumers to reliably use SPM&#39;s .upToNextMinor(from:) and .upToNextMajor(from:) to specify supported versions.</p><h3>How it can be achieved?</h3><p>Let’s break down our automation approach into clear, manageable steps:</p><ol><li><strong>Compare API states</strong>: We’ll extract the public interface from both the latest tagged version and the current codebase</li><li><strong>Analyze differences</strong>: We’ll identify what has been added, modified, or removed in the public API</li><li><strong>Apply versioning rules</strong>: Based on the changes detected, we’ll determine the appropriate version bump:<br>- <strong>Patch</strong> (x.x.Z) — bug fixes and internal changes that don’t affect the public API<br>- <strong>Minor</strong> (x.Y.x) — new features that are backward compatible<br>- <strong>Major</strong> (X.x.x) — breaking changes that require consumer code updates</li><li><strong>Propose version</strong>: We’ll generate the next appropriate version number following semantic versioning principles</li></ol><h3>Script foundation and argument parsing</h3><p>First, setup the script basics:</p><pre>#!/usr/bin/env bash<br><br>set -Eeuo pipefail<br>trap &#39;error_handler ${FUNCNAME-main context} ${LINENO} $?&#39; ERR<br><br># CONSTANTS<br><br>readonly CALL_DIR=&quot;$PWD&quot;<br>readonly SCRIPT_NAME=$(basename -s &quot;.sh&quot; &quot;$0&quot;)<br>readonly TEMP_DIRECTORY=&quot;tmp$RANDOM&quot;<br><br># FUNCTIONS<br><br>function error_handler() {<br>    echo &quot;$SCRIPT_NAME.sh: in &#39;$1()&#39;, line $2: error: $3&quot;<br>    reset<br>    exit 1<br>}<br><br>function main() {<br>    # We will fill it later.<br>}<br><br># ENTRY POINT<br><br>while [[ $# -gt 0 ]]; do<br>    case $1 in<br>        # Device for which public interface is created. Use value supported by `-destination` argument from `xcodebuild archive`,<br>        # e.g. `platform=iOS Simulator,name=iPhone 14,OS=17.0`.<br>        -d|--device)<br>            DESTINATION=${2}<br>            shift 2<br>        ;;<br>        # Derived data path (might be optional).<br>        -r|--derived-data-path)<br>            DERIVED_DATA_PATH=${2}<br>            ARCHIVE_PATH=&quot;$DERIVED_DATA_PATH/archive&quot;<br>            shift 2<br>        ;;<br>        # Package scheme name. For packages with multiple targets, it may be required to add `-Package` suffix.<br>        # Example: your package is named `ClientService` and has two targets inside: `ClientServiceDTOs` and `ClientServiceAPI`.<br>        # Then, your target would be `ClientService-Package`.<br>        -s|--scheme)<br>            SCHEME=${2}<br>            shift 2<br>        ;;<br>        *)<br>            echo &quot;Unknown parameter: &#39;${1}&#39;.&quot;<br>            exit 1<br>        ;;<br>    esac<br>done<br><br>main</pre><p>The argument parsing allows parametrized inputs, like:</p><pre>propose_next_version.sh \<br>    --device &quot;platform=iOS Simulator,name=iPhone 16,OS=18.0&quot; \<br>    --derived-data-path &quot;.build&quot; \<br>    --scheme &quot;FooBar&quot;</pre><p>Although $DERIVED_DATA_PATH is optional, I highly recommend using it. Due to different directory (I suggest local, e.g. ./.build) it prevents data collision issues when the script runs in parallel CI jobs on one runner.</p><p>Next, add some helpers which will be used later on:</p><pre>function reset() {<br>    cd &quot;$CALL_DIR&quot;<br>    rm -rf &quot;$TEMP_DIRECTORY&quot; &gt; /dev/null<br>}<br><br># Extract package name from Package.swift<br>function swift_package_name() {<br>    swift package describe --type json | jq -r .name<br>}</pre><p>These helper functions serve two purposes: reset() cleans up temporary files and returns to the original directory, while swift_package_name() extracts the package name from Package.swift using Swift&#39;s built-in package description and jq for JSON parsing. Make sure you have <a href="https://github.com/jqlang/jq">jq</a> installed.</p><h3>Finding the current version tag</h3><p>We need to identify the latest version to compare against. The script scans git history starting from the active branch HEAD to find the latest version tag:</p><pre>function get_current_version_tag_name() {<br>    local current_branch_name<br>    local last_reference_tag_name<br><br>    current_branch_name=$(git rev-parse --abbrev-ref HEAD)<br>    last_reference_tag_name=$(git tag --merged=&quot;$current_branch_name&quot; --list --sort=-version:refname &quot;[0-9]*.[0-9]*.[0-9]*&quot; | head -n 1)<br>    cat &lt;&lt;&lt; &quot;$last_reference_tag_name&quot;<br>}</pre><p>This function:</p><ol><li>Gets the current branch name. Usually it is default branch (main/master)</li><li>Lists all tags merged into the current branch that match semver pattern</li><li>Sorts them by version in reverse order (-version:refname), from latest to oldest</li><li>Takes the first (latest) one</li></ol><h3>Generating public API interfaces</h3><p>Swift provides built-in capabilities to generate public API files. We could have two different approaches depending on the package type:</p><p>For packages using Apple frameworks (like UIKit):</p><pre>function build_public_interface() {<br>    xcodebuild \<br>        -archivePath &quot;$ARCHIVE_PATH&quot; \<br>        -derivedDataPath &quot;$DERIVED_DATA_PATH&quot; \<br>        -destination &quot;$DESTINATION&quot; \<br>        -scheme &quot;$SCHEME&quot; \<br>        BUILD_LIBRARY_FOR_DISTRIBUTION=YES \<br>        SKIP_INSTALL=NO \<br>        OTHER_SWIFT_FLAGS=&quot;-no-verify-emitted-module-interface&quot; \<br>        archive | xcbeautify<br>}</pre><p>Be aware that the above solution works only on macOS.</p><p>For clean Swift packages (universal):</p><pre>function build_public_interface() {<br>    swift build \<br>        --build-path &quot;$DERIVED_DATA_PATH&quot; \<br>        -Xswiftc -enable-library-evolution \<br>        -Xswiftc -emit-module-interface \<br>        -Xswiftc -no-verify-emitted-module-interface | xcbeautify<br>}</pre><p>By default, after generating public interface, compiler verifies it. The -no-verify-emitted-module-interface parameter disables that verification. In our case we only need generated file and its verification is not required. <a href="https://github.com/cpisciotta/xcbeautify">xcbeautify</a> is not a mandatory here, but it is nice to have. It prints nicely the building output which we may use in case of encountered build error.</p><h3>Extracting the public interface</h3><p>After building the package, we need to extract its public interface for comparison. The function below automates this process:</p><pre>function get_public_interface() {<br>    local package_name<br>    local public_interface_directory<br>    local build_output<br><br>    if ! build_output=$(build_public_interface 2&gt;&amp;1); then<br>        echo &quot;❌ Build failed. Error details:&quot; &gt;&amp;2<br>        echo &quot;$build_output&quot; &gt;&amp;2<br>        echo &quot;Cannot complete the build due to the compile error. Check logs above.&quot; &gt;&amp;2<br>        exit 1<br>    fi<br><br>    package_name=$(swift_package_name)<br>    public_interface_directory=$(find &quot;./$DERIVED_DATA_PATH/&quot; -name &quot;${package_name}.swiftinterface&quot;)<br>    cat &lt;&lt;&lt;&quot;$(cat &quot;$public_interface_directory&quot;)&quot;<br>}</pre><p>This function:</p><ul><li>Builds the package and captures any compilation errors, exiting early if the build fails.</li><li>Determines the package name and locates the generated .swiftinterface file.</li><li>Outputs the contents of the public interface, which will be used for API comparison.</li></ul><h3>Creating temporary workspaces and comparing versions</h3><p>Let’s build now the main function. First, define local variabales, get branch name and latest version tag. Setup reporary directories and cleanup derived-data folder to avoid any possible data issues:</p><pre>local current_branch_name<br>local current_public_interface<br>local current_public_interface_path<br>local has_breaking_changes<br>local has_additive_changes<br>local normalized_derived_data_path<br>local normalized_temp_dir<br>local temp_diff_directory<br>local temp_version_directory<br>local version_public_interface<br>local version_public_interface_path<br>local version_tag<br><br>local -r semantic_version_regex=&#39;([0-9]+).([0-9]+).([0-9]+)&#39;<br><br>current_branch_name=$(git rev-parse --abbrev-ref HEAD)<br>version_tag=$(get_current_version_tag_name)<br><br>normalized_temp_dir=$(echo &quot;$TEMP_DIRECTORY&quot; | sed &#39;s/^\.\///&#39;)<br>normalized_derived_data_path=$(echo &quot;$DERIVED_DATA_PATH&quot; | sed &#39;s/^\.\///&#39;)<br><br>temp_diff_directory=&quot;$TEMP_DIRECTORY/diff&quot;<br>temp_version_directory=&quot;$TEMP_DIRECTORY/version&quot;<br><br># Clean up derived data directory to prevent usage of any cached files<br>rm -rf &quot;$DERIVED_DATA_PATH&quot;</pre><p>Next, create temporary copies of both the tagged version and current changes. They are going to be used for diffing later:</p><pre># Copy change tagged with given version tag to temporary directory<br>git clone &quot;$CALL_DIR&quot; --branch &quot;$version_tag&quot; --single-branch &quot;$temp_version_directory&quot; --quiet --recurse-submodules -c advice.detachedHead=false<br><br># Get public interface from the previous version<br>cd &quot;$temp_version_directory&quot;<br>version_public_interface=$(get_public_interface)<br><br># Go back to the project root.<br>cd &quot;$CALL_DIR&quot;<br><br># Get public interface from the current changes<br>current_public_interface=$(get_public_interface)<br><br># Save public interfaces for comparison<br>mkdir -p &quot;$temp_diff_directory&quot;<br>version_public_interface_path=&quot;$temp_diff_directory/version.swiftinterface&quot;<br>current_public_interface_path=&quot;$temp_diff_directory/current.swiftinterface&quot;<br><br># Save public API outputs without comments (comments can change without affecting API)<br>echo &quot;$version_public_interface&quot; | grep --invert-match &#39;^//&#39; &gt; &quot;$version_public_interface_path&quot;<br>echo &quot;$current_public_interface&quot; | grep --invert-match &#39;^//&#39; &gt; &quot;$current_public_interface_path&quot;</pre><h3>Analyzing differences and proposing versions</h3><p>The final step compares the two interface files using diff:</p><pre># Make public interfaces diffs<br>has_breaking_changes=$(diff &quot;$version_public_interface_path&quot; &quot;$current_public_interface_path&quot; | grep -c -i &quot;^&lt;&quot; || true)<br>has_additive_changes=$(diff &quot;$version_public_interface_path&quot; &quot;$current_public_interface_path&quot; | grep -c -i &quot;^&gt;&quot; || true)<br><br># Create version based on diff output<br>if [[ ! $version_tag =~ $semantic_version_regex ]]; then<br>    cat &lt;&lt;&lt; &quot;$version_tag&quot;<br>else<br>    local major=&quot;${BASH_REMATCH[1]}&quot;<br>    local minor=&quot;${BASH_REMATCH[2]}&quot;<br>    local patch=&quot;${BASH_REMATCH[3]}&quot;<br><br>    if [[ $has_breaking_changes -gt 0 ]]; then<br>        major=$((major+1))<br>        minor=0<br>        patch=0<br>    elif [[ $has_additive_changes -gt 0 ]]; then<br>        minor=$((minor+1))<br>        patch=0<br>    else<br>        patch=$((patch+1))<br>    fi<br><br>    cat &lt;&lt;&lt; &quot;${major}.${minor}.${patch}&quot;<br>fi</pre><p>The semantic version is parsed using regex. Based on the diff analysis:</p><ul><li><strong>Breaking changes</strong> (lines removed from the API, indicated by ^&lt; in diff output): increment major version, reset minor and patch to 0</li><li><strong>Additive changes</strong> (new lines added to the API, indicated by ^&gt; in diff output): increment minor version, reset patch to 0</li><li><strong>No API changes</strong>: increment only the patch version</li></ul><h3>Cleanup</h3><p>Finally, clean up the temporary directory to avoid leaving behind artifacts:</p><pre>reset</pre><h3>Practical usage examples</h3><p>The script above can be used to create a tag with every successful merge into the default branch (main/master). Here&#39;s a complete usage example:</p><pre># Basic usage<br>chmod +x &quot;propose_next_version.sh&quot; &amp;&amp; ./propose_next_version.sh \<br>    --device &quot;platform=iOS Simulator,name=iPhone 16,OS=18.0&quot; \<br>    --derived-data-path &quot;.build&quot; \<br>    --scheme &quot;MyPackage&quot;<br><br># In CI/CD pipeline<br>NEXT_VERSION=$(chmod +x &quot;propose_next_version.sh&quot; &amp;&amp; ./propose_next_version.sh --device &quot;platform=iOS Simulator,name=iPhone 16,OS=18.0&quot; --derived-data-path &quot;.build&quot; --scheme &quot;MyPackage&quot;)<br>git tag &quot;$NEXT_VERSION&quot;<br>git push origin &quot;$NEXT_VERSION&quot;</pre><h3>Conclusion</h3><p>By combining Swift’s built-in tools with shell scripting, we’ve created a solution for automating version proposals. This approach ensures consistent versioning practices and reduces human error in version management.</p><p>The complete code for this article is available in <a href="https://github.com/Filozoff/BlogArticles/tree/master/Article003">my GitHub repository</a>.</p><h3>Future improvements</h3><p>While described current solution provides a solid foundation, there are several areas where it could be enhanced:</p><ol><li><strong>Dependency analysis</strong>: Implement scanning of Package.swift for dependency version changes that might affect compatibility</li><li><strong>Package mutliple target support</strong>: This solution works well for one-target Swift packages. It can be improved to scan all targets within provided scheme and do diffing for all public interface files</li><li><strong>Custom API Analysis</strong>: Develop more sophisticated parsing for complex API changes, like treating new parameter with default value inside a method or enum as non-breaking change</li><li><strong>Beta releases support</strong>: The discussed script handles versioning for mature packages, where breaking changes bump the MAJOR version. For beta releases, breaking changes would bump the MINOR version instead</li><li><strong>Multiple device support</strong>: Some packages have platform-specific APIs for Linux or macOS. The current example requires developers to run the script individually for each supported platform and compare the resulting versions</li></ol><p>For GitHub users, instead of implementing the script from scratch, consider using a ready-made action from my repository: <a href="https://github.com/Filozoff/action-swift-propose-next-version">Filozoff/action-swift-propose-next-version</a>. You can see <a href="https://github.com/Filozoff/XCTestExtension/blob/master/.github/workflows/ci.yml">a live example of its usage</a> in my library.</p><p><em>Originally published at </em><a href="https://github.com/Filozoff/BlogArticles/tree/master/Article003"><em>https://github.com</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6bcba80e59f7" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Composing Accessibility Identifiers for SwiftUI Components]]></title>
            <link>https://medium.com/better-programming/composing-accessibility-identifiers-for-swiftui-components-10849847bd10?source=rss-c372b063c70e------2</link>
            <guid isPermaLink="false">https://medium.com/p/10849847bd10</guid>
            <category><![CDATA[accessibility-identifier]]></category>
            <category><![CDATA[swiftui]]></category>
            <category><![CDATA[ui-testing]]></category>
            <category><![CDATA[xctest]]></category>
            <category><![CDATA[xcuitest]]></category>
            <dc:creator><![CDATA[Kamil Wyszomierski]]></dc:creator>
            <pubDate>Fri, 14 Jul 2023 09:41:04 GMT</pubDate>
            <atom:updated>2023-07-26T16:57:57.252Z</atom:updated>
            <content:encoded><![CDATA[<h4>Learn how to compose Accessibility Identifiers with the same ergonomics as modifying View with padding, background or foreground colors</h4><figure><img alt="Magnifying glass above the phone showing accessibility identifiers" src="https://cdn-images-1.medium.com/max/1024/1*ZDikZsDPiJCPs_03vJLU0w.png" /><figcaption><a href="https://github.com/Filozoff/BlogArticles/blob/master/Article002/Resources/accessibility_identifiers.png">Image created by author using Procreate</a></figcaption></figure><h3>What Are Accessibility Identifiers, and What Are They For?</h3><p>Accessibility identifiers can be used to reliably distinguish an element (XCUIElement) in UI automated tests (XCUITests). The alternatives either have low performance (XPath) or require specific selectors that may randomly fail. Still, it may be useful to know them. To learn about other options, check <a href="https://www.browserstack.com/guide/xcuitest-locators-to-find-elements">BrowserStack’s guide</a> on finding elements.</p><p>For more information about accessibility identifiers, please visit <a href="https://developer.apple.com/documentation/appkit/nsaccessibility/1535023-accessibilityidentifier">Apple’s documentation</a>.</p><h3>How To Verify Them</h3><p>The best way is to preview XCUIElements by using inspector tools. XCUIElements are tree-structured, the same as views. There are two well-known ways to preview them.</p><h3>Accessibility Inspector</h3><p>The Accessibility Inspector is a tool provided with Xcode. It can be opened from Xcode’s menu: Xcode -&gt; Open Developer Tool -&gt; Accessibility Inspector or cmd+space and typing “accessibility inspector.”</p><p>While the app is running in the simulator, go to “All processes” in “Accessibility Inspector,” choose “Simulator,” and then click on the “Target an element” button (1). The “selection mode” will be enabled.</p><p>Click on the element you want to inspect (green overlay will assist with choosing the right one). The accessibility identifier is under “Identifier” in the “Advanced” section (2).</p><p>Here’s an example of Health.app:</p><figure><img alt="Health.app accessibility identifier preview" src="https://cdn-images-1.medium.com/max/1024/1*SOnAT_M1Ursp9dOQ9nes8Q.png" /><figcaption>Health.app accessibility identifier preview using Accessibility Inspector</figcaption></figure><p>Accessibility Inspector comes with amazing tools that help you improve the accessibility of your app (e.g., contrast ratio scans). To learn more about Accessibility Inspector and its features, please watch a <a href="https://developer.apple.com/videos/play/wwdc2019/257">WWDC session from 2019</a>.</p><h3>Appium Inspector</h3><p>Appium Inspector requires more work to set up, but in the end, it is more convenient for accessibility identifier preview and provides data in a readable form.</p><h4>Installation</h4><p>Several tutorials will help you install Appium Inspector, but if you want to save some time, feel free to use the one below:</p><p>Appium Inspector requires npm and Appium Server. The simplest way is to type these commands in the terminal and install it through <a href="https://brew.sh/">homebrew</a>:</p><pre>brew install node # installs npm</pre><pre>npm install -g appium@next # installs appium server through npm</pre><p>You can check the Appium installation by printing its version. You can do it by typing the following in the terminal:</p><pre>appium -v</pre><p>To run XCUITests or scan the tree structure, Appium needs to install an additional driver. To install the XCUITest driver, type the following in the terminal:</p><pre>appium driver install xcuitest # installs Appium driver which allows to run XCUITest</pre><p>To check if the driver was successfully installed, you can create a list of the installed drivers using the terminal:</p><pre>appium driver list --installed</pre><p>Next, download Appium Inspector’s installation package from <a href="https://github.com/appium/appium-inspector/releases">Appium’s GitHub</a>. In case of errors, please follow <a href="https://github.com/appium/appium-inspector#installation">Appium’s installation guide</a>.</p><h4>Usage</h4><p>First, run the Appium server by typing this command in the terminal:</p><pre>appium server</pre><p>Then, open the Appium Inspector app and define properties (1). At a minimum, you have to define platformName and appium:automationName as follows:</p><ul><li>platformName: use iOS</li><li>appium:automationName: use XCUITest</li></ul><p>You can save that configuration (it is not saved by default). To start a screen scanning process, click the “Start Session” button (2).</p><figure><img alt="Appium Inspector setup" src="https://cdn-images-1.medium.com/max/1024/1*VMH9rk1TqI6AokNrIf5dng.png" /><figcaption>Appium Inspector setup</figcaption></figure><p>To learn more about available options (like running on a physical device), please visit <a href="https://appium.io/docs/en/2.0/guides/caps/">Appium’s guide</a>.</p><p>Click on the element you want to inspect (an overlay will assist with choosing the right one). Here’s an example of Health.app (with the same screen and identifier used in the previous example above):</p><figure><img alt="Health.app accessibility identifier preview using Appium Inspector" src="https://cdn-images-1.medium.com/max/1024/1*n2jG4NHZraSR4Ygg4CaUpQ.png" /><figcaption>Health.app accessibility identifier preview using Appium Inspector</figcaption></figure><p>Use the “Refresh” button from the top list to refresh the screen and see the updated view data.</p><h3>Composing IDs</h3><p>Now, as you know how to verify the identifiers, it’s time to compose them. We are going to use a tree structure.</p><figure><img alt="Tree structure" src="https://cdn-images-1.medium.com/max/271/1*0yytG-OSgHhjvJffq8Elsw.png" /></figure><p>A tree is constructed with branches (1, 2) and leaves (3, 4, 5). Like regular trees, a leaf cannot exist without at least one branch.</p><p>Take a look at the identifier from Health.app: UIA.Health.ReviewHealthChecklistTile.Header.Title. A dot separates each of its parts and defines the next level of nesting, building a tree structure. In that case, the following parts: UIA, Health, ReviewHealthChecklistTile, and Header are branches and Title is a leaf.</p><p>As you may notice, a tree structure has the following key advantages:</p><ul><li>it allows better identifier organization</li><li>each nesting level provides more detail about the view placement, so it is easier to find it in a view hierarchy</li></ul><p>The example of Apple’s Health app is constructed by using the following format: Purpose.App.ViewGroup.Component.Element, but feel free to define your own (e.g., it is absolutely fine to use ScreenName.ViewGroup.Component.Element instead).</p><p>Composing identifiers can be done by simply passing the identifier through a constructor like this:</p><pre>struct UserDetails: View {<br><br>    let name: String<br>    private let parentIdentifier: String<br><br>    init(name: String, parentIdentifier: String) {<br>        self.name = name<br>        self.parentIdentifier = parentIdentifier<br>    }<br><br>    var body: some View {<br>        Text(name)<br>            .accessibilityIdentifier(&quot;\(parentIdentifier).Label&quot;)<br>    }<br>}</pre><p>However, it is not flexible enough. First, it introduces an additional dependency passed through the constructor, polluting its API with UI-related dependency. Second, you lose an option to set an “identifier branch” to regular SwiftUI view containers, like VStack. The best option would be to allow an identifier to be created in the same way that sets up a background color or adds padding to a View or a group of them.</p><p>To achieve that, we are going to use custom EnvironmentKey and ViewModifier. Let’s start by creating a dedicated EnvironmentKey:</p><pre>import SwiftUI<br><br>struct ParentAccessibilityBranchKey: EnvironmentKey {<br><br>    static let defaultValue: String? = nil<br>}<br><br>extension EnvironmentValues {<br><br>    var parentAccessibilityBranch: String? {<br>        get { self[ParentAccessibilityBranchKey.self] }<br>        set { self[ParentAccessibilityBranchKey.self] = newValue }<br>    }<br>}</pre><p>That environment value will be used to pass a branch name from the parent View, to its children. Once set to a parent, it is visible to all child views.</p><p>Next, let’s create a dedicated ViewModifier to construct a branch and leaf.</p><h4>Branch modifier</h4><pre>import SwiftUI<br><br>public struct AccessibilityIdentifierBranchModifier: ViewModifier {<br><br>    @Environment(\.parentAccessibilityBranch) private var parentBranch<br><br>    private let branch: String<br><br>    public init(branch: String) {<br>        self.branch = branch<br>    }<br><br>    public func body(content: Content) -&gt; some View {<br>        content<br>            .environment(\.parentAccessibilityBranch, makeGroupPath())<br>    }<br><br>    private func makeGroupPath() -&gt; String {<br>        guard let parentBranch = parentBranch else { return branch }<br>        return &quot;\(parentBranch).\(branch)&quot;<br>    }<br>}<br><br>public extension View {<br><br>    func accessibilityIdentifierBranch(_ branch: String) -&gt; ModifiedContent&lt;Self, AccessibilityIdentifierBranchModifier&gt; {<br>        modifier(AccessibilityIdentifierBranchModifier(branch: branch))<br>    }<br>}</pre><p>The branch modifier takes a branch from a parent View and appends to it the branch provided in the constructor. The modifier only constructs the identifier but does not apply it to a View.</p><h4>Leaf modifier</h4><pre>public struct AccessibilityIdentifierLeafModifier: ViewModifier {<br><br>    @Environment(\.parentAccessibilityBranch) private var branch<br><br>    private let leaf: String<br><br>    public init(leaf: String) {<br>        self.leaf = leaf<br>    }<br><br>    public func body(content: Content) -&gt; some View {<br>        if let branch = branch {<br>            content<br>                .accessibilityIdentifier(&quot;\(branch).\(leaf)&quot;)<br>                .environment(\.parentAccessibilityBranch, nil)<br>        } else {<br>            content<br>        }<br>    }<br>}<br><br>public extension View {<br><br>    func accessibilityIdentifierLeaf(_ leaf: String) -&gt; ModifiedContent&lt;Self, AccessibilityIdentifierLeafModifier&gt; {<br>        modifier(AccessibilityIdentifierLeafModifier(leaf: leaf))<br>    }<br>}</pre><p>The leaf modifier applies the identifier to a View. You may have noticed, that the parentAccessibilityBranch environment value is nullified for the leaf modifier. It prevents further branching creation because growing a branch from a leaf is not feasible. The leaf modifier closes the identifier creation.</p><p>Once the implementation is done, let’s create an example of a reusable View:</p><pre>struct UserDetails: View {<br><br>    let name: String<br>    init(name: String) {<br>        self.name = name<br>    }<br><br>    var body: some View {<br>        HStack {<br>            Image(systemName: &quot;person.circle&quot;)<br>                .resizable()<br>                .aspectRatio(contentMode: .fit)<br>                .frame(width: 40.0)<br>            VStack(alignment: .leading) {<br>                Text(&quot;Name&quot;)<br>                    .foregroundColor(.secondary)<br>                    .font(.caption)<br>                    .accessibilityIdentifierLeaf(&quot;Label&quot;)<br>                Text(name)<br>                    .accessibilityIdentifierLeaf(&quot;Value&quot;)<br>            }<br>        }<br>        .padding()<br>        .frame(maxWidth: .infinity, alignment: .leading)<br>        .background(.thinMaterial)<br>        .cornerRadius(10.0)<br>    }<br>}</pre><p>and use it by another View with identifier modifiers:</p><pre>struct ContentView: View {<br><br>    var body: some View {<br>        VStack {<br>            UserDetails(name: &quot;Bugs Bunny&quot;)<br>                .accessibilityIdentifierBranch(&quot;UserDetails&quot;)<br>        }<br>        .padding()<br>        .accessibilityIdentifierBranch(&quot;Users&quot;)<br>    }<br>}</pre><p>As a result of the above, let’s take a look at the preview to verify the identifier:</p><figure><img alt="Code example identifier preview" src="https://cdn-images-1.medium.com/max/1024/1*NXtC5Ky1iqSw9mP-XPOEPQ.jpeg" /><figcaption>Code example identifier preview</figcaption></figure><h3>Conclusion</h3><p>Setting up accessibility identifiers with custom SwiftUI components can be flexible without introducing an additional element to the Views constructor. SwiftUI provides an API that allows composing identifiers with the same ergonomics as modifying a View with padding, background, and foreground color.</p><p>Although the solution above is flexible, consider building the identifier tree with caution to prevent introducing frequent changes that may break the UI tests.</p><p>You may find the complete code in <a href="https://github.com/Filozoff/BlogArticles/tree/master/Article002">my GitHub repository</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=10849847bd10" width="1" height="1" alt=""><hr><p><a href="https://medium.com/better-programming/composing-accessibility-identifiers-for-swiftui-components-10849847bd10">Composing Accessibility Identifiers for SwiftUI Components</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[Passing dependencies to ViewModel in SwiftUI]]></title>
            <link>https://medium.com/@kamil.wyszomierski/passing-dependencies-to-viewmodel-in-swiftui-572da1b67b42?source=rss-c372b063c70e------2</link>
            <guid isPermaLink="false">https://medium.com/p/572da1b67b42</guid>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[mvvm]]></category>
            <category><![CDATA[swiftui]]></category>
            <category><![CDATA[dependency-injection]]></category>
            <dc:creator><![CDATA[Kamil Wyszomierski]]></dc:creator>
            <pubDate>Fri, 24 Feb 2023 16:09:13 GMT</pubDate>
            <atom:updated>2023-06-06T20:26:08.357Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EG-NECs-LlY-18nR27Q6ow.jpeg" /><figcaption>SwiftUI Icon from <a href="https://developer.apple.com/news/?id=zgtznbbc">https://developer.apple.com/news/?id=zgtznbbc</a></figcaption></figure><p>When taking the first steps with SwiftUI, this runtime warning is very often encountered:</p><blockquote><em>“Accessing StateObject’s object without being installed on a View. This will create a new instance each time.”</em></blockquote><p>… which can be frustrating at the beginning, especially for devs who come from “ UIKit world&quot;. Paying attention to this message is strongly recommended, because that highlighted code may not only impact the performance but the logic as well. Besides, the way how the dependencies are passed defines API design and construction of Unit Tests.</p><p>So then, how to pass dependency to a ViewModel safely and reliably?</p><p>See the proposals below.</p><h3>Through onAppear</h3><p>One of the solutions is passing dependencies when we are 100% sure that it happens after a view’s creation: through onAppear modifier. Let&#39;s take a look at the code:</p><p>ViewModel:</p><pre>@MainActor<br>class ViewOneViewModel: ObservableObject {<br><br>    @Published var id = &quot;nil&quot;<br>    @Published var name = &quot;&quot;<br><br>    var userRepository: UserRepository?<br><br>    @Published private(set) var isFetchingData = false<br><br>    private var currentTask: Task&lt;Void, Error&gt;?<br><br>    init() { }<br><br>    deinit {<br>        currentTask?.cancel()<br>    }<br><br>    private func fetchUserData() {<br>        guard !isFetchingData else { return }<br>        isFetchingData = true<br>        currentTask = Task { [weak self] in<br>            do {<br>                guard let id = self?.id,<br>                      let repository = self?.userRepository<br>                else { return }<br><br>                let name = try await repository.fetchName(id: id)<br>                self?.name = name<br>            } catch {<br>                print(error.localizedDescription)<br>            }<br><br>            self?.isFetchingData = false<br>        }<br>    }<br>}<br><br>// MARK: - Actions<br><br>extension ViewOneViewModel {<br><br>    func onAppear() {<br>        fetchUserData()<br>    }<br>}</pre><p>View :</p><pre>struct ViewOne: View {<br><br>    let id: String<br>    let userRepository: UserRepository<br><br>    @StateObject private var viewModel = ViewOneViewModel()<br><br>    init(id: String, userRepository: UserRepository) {<br>        self.id = id<br>        self.userRepository = userRepository<br>    }<br><br>    var body: some View {<br>        VStack(alignment: .leading) {<br>            Text(&quot;Your ID: \(viewModel.id)&quot;)<br>            HStack {<br>                Text(&quot;Name: &quot;)<br>                TextField(&quot;type name&quot;, text: $viewModel.name)<br>                if viewModel.isFetchingData {<br>                    ProgressView()<br>                        .progressViewStyle(.circular)<br>                }<br>            }<br>            .padding()<br>            .background(.quaternary)<br>            .cornerRadius(10)<br>            .disabled(viewModel.isFetchingData)<br><br>            NavigationLink(&quot;Next View&quot;, value: RootView.Page.nextView)<br>        }<br>        .padding()<br>        .onAppear {<br>            viewModel.id = id<br>            viewModel.userRepository = userRepository<br>            viewModel.onAppear()<br>        }<br>    }<br>}</pre><p>At the current stage, the View resets its state with every &quot;appear&quot; action, e.g. when navigating back or switching tabs. Ideally would be, to assign properties only &quot;on View loaded&quot; event. Due to the lack of such a modifier in SwiftUI, it is required to create a custom one to tackle this issue:</p><pre>struct OnLoadViewModifier: ViewModifier {<br><br>    typealias Action = () -&gt; ()<br><br>    @State private var isLoaded = false<br><br>    private let action: Action<br><br>    init(action: @escaping Action) {<br>        self.action = action<br>    }<br><br>    func body(content: Content) -&gt; some View {<br>        content<br>            .onAppear {<br>                guard !isLoaded else { return }<br>                isLoaded = true<br>                action()<br>            }<br>    }<br>}<br><br>extension View {<br><br>    func onLoad(perform action: @escaping OnLoadViewModifier.Action) -&gt; some View {<br>        modifier(OnLoadViewModifier(action: action))<br>    }<br>}</pre><p>Updated ViewOne with a new modifier:</p><pre>struct ViewOne: View {<br><br>    (...)<br><br>    var body: some View {<br>        VStack(alignment: .leading) { (...) }<br>        .padding()<br>        .onLoad {<br>            viewModel.id = id<br>            viewModel.userRepository = userRepository<br>        }<br>        .onAppear {<br>            viewModel.onAppear()<br>        }<br>    }<br>}</pre><p>As you may see, with this solution ViewOneViewModel requires to have either pre-defined, optional, or late init values. ViewOne&#39;s properties are used to be passed further to ViewModel only. That duplication creates API pollution which may lead to the wrong adaptation by other developers e.g. by using properties from a View instead of a ViewModel. Besides, the code requires more attention during its maintenance.</p><h3>Through constructor</h3><p>This solution is based on passing dependencies when @StateObject property wrapper calls its closure (i.e. when a View is created).</p><p>ViewModel :</p><pre>@MainActor<br>class ViewTwoViewModel: ObservableObject {<br><br>    @Published var name = &quot;&quot;<br><br>    let id: String<br><br>    @Published private(set) var isFetchingData = false<br><br>    private var currentTask: Task&lt;Void, Error&gt;?<br>    private let userRepository: UserRepository<br><br>    init(id: String, userRepository: UserRepository) {<br>        self.id = id<br>        self.userRepository = userRepository<br>    }<br><br>    deinit {<br>        currentTask?.cancel()<br>    }<br><br>    private func fetchUserData() {<br>        guard !isFetchingData else { return }<br>        isFetchingData = true<br>        currentTask = Task { [weak self] in<br>            do {<br>                guard let id = self?.id,<br>                      let repository = self?.userRepository<br>                else { return }<br><br>                let name = try await repository.fetchName(id: id)<br>                self?.name = name<br>            } catch {<br>                print(error.localizedDescription)<br>            }<br><br>            self?.isFetchingData = false<br>        }<br>    }<br>}<br><br>// MARK: - Actions<br><br>extension ViewTwoViewModel {<br><br>    func onAppear() {<br>        fetchUserData()<br>    }<br>}</pre><p>View :</p><pre>typealias ReturnClosure&lt;T&gt; = () -&gt; T<br><br>struct ViewTwo: View {<br><br>    @StateObject private var viewModel: ViewTwoViewModel<br><br>    init(viewModel: @escaping @autoclosure ReturnClosure&lt;ViewTwoViewModel&gt;) {<br>        _viewModel = .init(wrappedValue: viewModel())<br>    }<br><br>    var body: some View {<br>        VStack(alignment: .leading) {<br>            Text(&quot;Your ID: \(viewModel.id)&quot;)<br>            HStack {<br>                Text(&quot;Name: &quot;)<br>                TextField(&quot;type name&quot;, text: $viewModel.name)<br>                if viewModel.isFetchingData {<br>                    ProgressView()<br>                        .progressViewStyle(.circular)<br>                }<br>            }<br>            .padding()<br>            .background(.quaternary)<br>            .cornerRadius(10)<br>            .disabled(viewModel.isFetchingData)<br><br>            NavigationLink(&quot;Next View&quot;, value: RootView.Page.nextView)<br>        }<br>        .padding()<br>        .onAppear {<br>            viewModel.onAppear()<br>        }<br>    }<br>}</pre><p>The View&#39;s call looks quite neat too and matches the declarative style:</p><pre>ViewTwo(<br>    viewModel: .init(<br>        id: &quot;123&quot;,<br>        userRepository: userRepository<br>    )<br>)</pre><p>This option is based on <a href="https://www.swiftui-lab.com/random-lessons#data-10">Q&amp;A session notes from SwiftUI Lab at WWDC</a> (I strongly encourage checking the whole article) and <a href="https://forums.swift.org/t/why-swiftui-state-property-can-be-initialized-inside-init-this-other-way/62772">the discussion from Swift Forum</a>.</p><p>Edit 2:</p><p>During the WWDC 2023 that topic was raised on WWDC’s Slack channel by @Tudor-Mihai and answered by Apple employee:</p><figure><img alt="Apple employee answer from WWDC `swiftui` channel regarding `@StateObject`" src="https://cdn-images-1.medium.com/max/1024/1*FEcbxo4edz9xiGTrYziblA.png" /><figcaption>Apple employee answer from WWDC `swiftui` channel regarding `@StateObject`</figcaption></figure><p>For @Observable macro and @State introduced at WWDC 2023 it looks the same:</p><figure><img alt="Apple employee answer on constructor injection while using @Observable macro" src="https://cdn-images-1.medium.com/max/1024/1*oL3BqWyMTKTcit4uRQb2OQ.png" /><figcaption>Apple employee answer on constructor injection while using @Observable macro</figcaption></figure><p>It looks more clear than the previous one, however, also comes with limitations. In the case of passing a value type, bear in mind that its mutation does not update the View as it is taken only once. Make sure the passed value type is up to date.</p><p>That does not affect a reference type or the Binding.</p><h3>@StateObject vs @ObservedObject</h3><p>To choose the right property wrapper it is important to know who owns a ViewModel.</p><p>The examples above are using @StateObject because ViewOne and ViewTwo are the only owners. @StateObject calls it&#39;s closure after View&#39;s load and keep closure&#39;s result until View deallocation. That makes ViewModel created only once.</p><p>Using @ObservedObject with a View as an owner makes ViewModel recreating with every View&#39;s redraw. @ObservedObject is a good choice as a child ViewModel, e.g. for a table cell, where the child is held by a parent ViewModel.</p><h3>Conclusion</h3><p>Passing dependencies to aViewModel in SwiftUI may look very difficult at the first sight, however, it could be easily manageable. It is important to take into consideration all limitations and a data type while passing to a ViewModel. You may find working examples in <a href="https://github.com/Filozoff/BlogArticles/tree/master/Article001">my GitHub repository</a>.</p><p>Edit 1: Changed code example to more complex and added section about property wrappers.</p><p>Edit 2: Update with information received from WWDC 2023 swiftui channel.</p><p><em>Originally published at </em><a href="https://github.com/Filozoff/BlogArticles/blob/master/Article001/article001.md"><em>https://github.com</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=572da1b67b42" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>