Skip | Bloghttps://skip.dev/enSkip Is Now Free and Open Sourcehttps://skip.dev/blog/skip-is-free/https://skip.dev/blog/skip-is-free/Wed, 21 Jan 2026 00:00:00 GMT<aside aria-label="TL;DR"><p aria-hidden="true">TL;DR</p><div><p>Skip is now completely free and open source. <a href="https://skip.dev/sponsor">Become a sponsor</a> to help sustain the future of truly native cross-platform development.</p></div></aside>
<p>Since launching Skip in 2023, we’ve pursued one mission: enable developers to create premium mobile apps for iOS and Android from a single Swift and SwiftUI codebase — without any of the compromises that have encumbered cross-platform development tools since, well, forever.</p>
<p>Over the past three years, Skip has evolved significantly. We started with a Swift-to-Kotlin transpiler and Android support for the most common SwiftUI APIs. We then founded the <a href="https://www.swift.org/android-workgroup/" rel="nofollow" target="_blank">Swift Android Workgroup<span> ↗</span></a> and released the Swift Android SDK to compile Swift natively for Android. We now have dozens of popular integration frameworks, interoperate with thousands of cross-platform Swift packages, and feature the most complete independent SwiftUI implementation available.</p>
<div><h3 id="the-challenge-of-paid-developer-tools">The Challenge of Paid Developer Tools</h3></div>
<p>Until today, Skip has required a paid subscription and license key to build apps. While free apps and indie developers below a revenue threshold were exempt, businesses were expected to subscribe. This model helped us bootstrap Skip without outside investment, but we’ve always known that to truly compete with legacy cross-platform tools and achieve widespread adoption, Skip would need to become freely available.</p>
<p>The plain truth is that developers expect to get their tools free of charge. First-party IDEs like Xcode and Android Studio, popular integration frameworks, and essential dev tools are all given away at no (direct) cost. The platform vendors monetize through developer program fees, app store commissions, and cloud services. Framework providers typically monetize through complementary services. But developer tools? Those have historically required the patronage of massive tech companies in order to fund their ongoing development, support, and infrastructure costs.</p>
<p>Beyond pricing, there’s a deeper concern about durability. Developers are understandably wary of building their entire app strategy on a small company’s paid, closed-source tool. What if the company goes under? Gets acquired and shut down? What happens to their apps? <em>We get it</em>. While Skip’s innate ejectability offers some risk mitigation, product teams need absolute confidence that their chosen technologies will be around next week, next year, and beyond. They must remain immune from the dreaded “rug pull” that so often accompanies a “pivot”.</p>
<p>To keep the development community’s trust and achieve mass adoption, Skip needs a completely free and open foundation. Even if the core team disappeared, the community could continue supporting the technology and the apps that depend on it.</p>
<div><h3 id="whats-changing">What’s Changing</h3></div>
<p>As of Skip 1.7, all licensing requirements have been removed. No license keys, no end-user license agreements, no trial or evaluation period.</p>
<ul>
<li><strong>Current Skip developers</strong>: Your setup remains completely unchanged, except you will no longer need your license key after upgrading.</li>
<li><strong>New Skip users</strong>: You can start building immediately — no evaluation license required.</li>
<li><strong>Open source skipstone</strong>: We’ve open-sourced the Skip engine, known as “skipstone”. This is the tool that handles all the critical build-time functionality: Project creation and management, Xcode and SwiftPM plugin logic, iOS-to-Android project transformation, resource and localization bundling, JNI bridge creation, source transpilation, app packaging, and project export. It is now available as a public GitHub repository at <a href="https://github.com/skiptools" rel="nofollow" target="_blank">https://github.com/skiptools<span> ↗</span></a> under a free and open-source license.</li>
<li><strong>Migrate skip.tools to skip.dev</strong>: As part of this process, we are launching our new home at <a href="https://skip.dev" rel="nofollow" target="_blank">https://skip.dev<span> ↗</span></a>! This new site hosts our documentation, blog, and case studies, and it is also open-source and welcomes contributions at <a href="https://github.com/skiptools/skip.dev" rel="nofollow" target="_blank">https://github.com/skiptools/skip.dev<span> ↗</span></a>. We will eventually be migrating the entirety of <a href="https://skip.tools" rel="nofollow" target="_blank">https://skip.tools<span> ↗</span></a> to <a href="https://skip.dev" rel="nofollow" target="_blank">https://skip.dev<span> ↗</span></a>.</li>
</ul>
<div><h3 id="supporting-skips-future">Supporting Skip’s Future</h3></div>
<p>Since day one, Skip has been bootstrapped. We haven’t taken venture capital or private equity investment, nor are we controlled by big tech. This independence means we control our destiny and can make the best decisions for Skip’s developers and users — a unique position in the cross-platform development space.</p>
<p>But independence requires community support. And that is where you come in.</p>
<ul>
<li><strong>Current subscribers</strong>: Your Small Business or Professional plan will automatically transition to an <a href="https://skip.dev/sponsor">Individual</a> or <a href="https://skip.dev/sponsor">Supporter</a> tier, respectively. You can cancel any time with no consequences (other than making us sad), but we hope you’ll consider staying on, at least throughout this transition period.</li>
<li><strong>Individual developers</strong>: If you believe in Skip’s mission, please consider supporting us through <a href="https://github.com/sponsors/skiptools" rel="nofollow" target="_blank">GitHub Sponsors<span> ↗</span></a> with a monthly contribution.</li>
<li><strong>Companies and organizations</strong>: For businesses that want to see Skip flourish, we offer corporate sponsorship tiers with visibility on our homepage and in our documentation. Your sponsorship directly funds development of the integration frameworks essential to production apps, as well as the ongoing maintenance, support, and infrastructure. Sponsorship comes with some compelling perks! Please visit <a href="https://skip.dev/sponsor">https://skip.dev/sponsor</a> to see the sponsorship tiers.</li>
</ul>
<p>Investing in Skip is also investing in your own team’s capabilities and competitive advantage. Your support accelerates Skip’s development and ensures its long-term success, enabling your developers to build exceptional native experiences efficiently, today and into the future.</p>
<div><h3 id="what-comes-next">What Comes Next</h3></div>
<p>We’re at a pivotal moment in the app development field. Legacy cross‑platform frameworks are struggling to keep pace with the rapid evolution of modern UI systems like Liquid Glass on iOS and Material Expressive on Android. The compromises that once felt acceptable in exchange for a unified codebase now result in dated interfaces, weaker user experiences, and real competitive disadvantages. Teams ready to move beyond those trade‑offs can count on Skip to champion what matters most: delivering truly native, uncompromised experiences on both major mobile platforms.</p>
<p>Opening Skip to the community marks the next step in its evolution. Software is never finished — especially a tool that supports modern Swift and Kotlin, SwiftPM and Gradle, Xcode and Android Studio, iOS and Android, and the ongoing growth of SwiftUI and Jetpack Compose. It’s a demanding pursuit, and we’re committed to it. But sustaining and expanding this work depends on the support of developers who believe in Skip’s mission.</p>
<p>Together, we will continue building toward Skip’s vision: a genuinely no‑compromise, cross‑platform foundation for universal mobile apps.</p>
<p>Thank you for your support, and as always, Happy Skipping!</p>
<hr>
<p><strong>Ready to get started?</strong> <a href="https://skip.dev/docs/gettingstarted/">Get started</a> with Skip 1.7 today and join the community building the future of native cross-platform development.</p>skipopen-sourcelicensingannouncementsustainabilitycross-platformSkip 2025 Retrospective and 2026 Roadmaphttps://skip.dev/blog/skip-2026/https://skip.dev/blog/skip-2026/Tue, 23 Dec 2025 00:00:00 GMT<p>As 2025 comes to a close, we’ve been reflecting on how far Skip has advanced this year. What began nearly three years ago with a simple desire to enable cross-platform app development with Swift and SwiftUI has grown into a thriving ecosystem, a strong community of developers and contributors, and a platform powering real production apps across iOS and Android.</p>
<p>This year wasn’t just about growth in numbers. It was about expanding depth and breadth: deeper integrations, stronger foundations, and a clearer vision for the future of native Swift across the dominant mobile platforms.</p>
<div><h2 id="native-swift-on-android-becomes-officially-supported">Native Swift on Android Becomes <em>Officially</em> Supported</h2></div>
<p>The highlight of 2025 by far was the official release of the Swift SDK for Android on swift.org, along with Skip’s support in the form of Skip Fuse. Prior to the advent of Skip Fuse, Skip operated solely in <em>transpiled</em> mode (now called “Skip Lite”), which converts Swift source code to Kotlin. Skip Fuse, on the other hand, builds natively-compiled Swift targeting the Android platform, which both eliminates the limitations imposed by source transpilation, as well as unlocks the universe of thousands of native Swift packages that are compatible with the Android platform.</p>
<p>Interest in the Swift SDK for Android has exploded since the <a href="https://www.swift.org/blog/nightly-swift-sdk-for-android/" rel="nofollow" target="_blank">initial announcement<span> ↗</span></a> and <a href="https://www.swift.org/blog/exploring-the-swift-sdk-for-android/" rel="nofollow" target="_blank">follow-up<span> ↗</span></a> blog posts on swift.org. We at Skip are proud to be founding members of the <a href="https://www.swift.org/android-workgroup/" rel="nofollow" target="_blank">Swift Android workgroup<span> ↗</span></a>, and we are committed to the platform’s enduring stability and support. And where the scope of the workgroup ends, we complete the picture by providing the tooling, libraries, and support needed to build universal apps from a single Swift codebase.</p>
<div><h2 id="liquid-glass-and-the-wisdom-of-staying-native">Liquid Glass and the Wisdom of Staying Native</h2></div>
<p>The launch of iOS 26 and the emergence of Liquid Glass as the new interface style was a pivotal moment for the cross-platform app development technosphere, as well as a powerful validation of Skip’s core philosophy. From day one, Skip has avoided intermediating or re-implementing SwiftUI on iOS or other Apple platforms. By staying fully native, Skip was able to support Liquid Glass on Day 1, automatically benefiting every Skip-based app without rewrites or workarounds (see our <a href="https://skip.dev/blog/skip-next-gen-mobile-ui/">blog post</a> on the topic).</p>
<p>In contrast, other cross-platform toolkits — such as Flutter and Compose Multiplatform — have found themselves stranded, incapable of adopting Liquid Glass and stuck on the previous UI generation with their mimicked faux-native components. For iOS users, that means outdated interfaces and an exacerbation of an already uncanny-valley non-native experience. For developers, it means frustration, limitation, and an inability to achieve the highest-quality app experience that their businesses demand.</p>
<p>Skip’s belief is that by embracing native platforms wholly — not abstracting them away — is the best path forward, both for users and developers. SkipUI maps un-intermediated SwiftUI on iOS to native Jetpack Compose on Android, guaranteeing that the user experience is always performant and familiar to users of the respective platforms.</p>
<div><h2 id="the-rapidly-expanding-skip-ecosystem">The Rapidly Expanding Skip Ecosystem</h2></div>
<p>A stock Skip app has just a few core dependencies: SkipUI, which provides a bridge from the SwiftUI API to a native Compose UI on Android, along with SkipFoundation, SkipModel, and SkipLib. But Skip also facilitates a thriving ecosystem of <em>optional</em> libraries, providing features and integrations that unlock the vast capabilities of third-party libraries and services and provide a unified dual-platform API surface.</p>
<p>Throughout 2025, Skip’s library ecosystem has matured and expanded dramatically. The community and core team introduced a wide range of new dual-platform frameworks designed to solve real-world problems without compromise. Some of our most popular integrations, like SQLite, Bluetooth, Firebase, Supabase, and WebView, have improved greatly through the help of outside contributions. These APIs were refined, edge cases were resolved, documentation improved, and real production feedback shaped work on of these frameworks.</p>
<p>In addition, we have some newer entrants to the Skip ecosystem, including:</p>
<ul>
<li>SkipNFC and SkipDevice for unlocking low-level hardware capabilities</li>
<li>SkipStripe for Stripe for payments and subscriptions</li>
<li>SkipPostHog for analytics and product insights</li>
<li>SkipAuth0 for authentication and identity</li>
<li>SkipSocketIO for real-time communication through the Socket.IO libraries</li>
</ul>
<p>These integration frameworks aren’t always just simple wrappers; they are designed to feel idiomatic in Swift, be composable with SwiftUI, and act faithfully with each platform’s underlying capabilities. And all of these platforms work equally with with transpiled Skip Lite as well as compiled Skip Fuse. A partial list of these Skip modules can be found at the <a href="https://skip.dev/docs/modules/">Skip Module Index</a>.</p>
<div><h2 id="looking-ahead-to-2026">Looking Ahead to 2026</h2></div>
<p>As exciting as 2025 was, we’re even more energized by what’s ahead. Our roadmap for 2026 includes:</p>
<ul>
<li>A growing catalog of integration frameworks for popular libraries, services, and backend platforms</li>
<li>Continued expansion and refinement of SkipFuse and Swift-on-Android tooling</li>
<li>Performance improvements, better diagnostics, and enhanced developer ergonomics</li>
<li>Enhanced IDE integration, both with our existing Xcode support as well as emerging alternatives for iOS development</li>
<li>A new series of deep-dive blog posts exploring real-world Skip architectures, advanced SwiftUI patterns, and platform-specific best practices</li>
</ul>
<p>Most importantly, we’ll continue building Skip in close collaboration with the community that made this year possible. If you haven’t yet tried Skip, there’s no better time than now to sign up for your <a href="https://skip.dev/eval/">free evaluation</a> and start creating universal mobile apps that are free from compromises.</p>
<p>As always, Happy Skipping, and Happy New Year!</p>skipretrospectiveroadmapcross-platformmobile-developmentswiftswiftuiandroidjetpack-composeskip-fuseswift-sdk-androidliquid-glassios-2620252026An official Swift SDK for Androidhttps://skip.dev/blog/official-swift-sdk-for-android/https://skip.dev/blog/official-swift-sdk-for-android/Sat, 25 Oct 2025 00:00:00 GMT<p>When we first launched Skip in 2023, the notion of using Swift to create universal mobile applications was novel. While some projects had dabbled with custom Swift toolchains to share some of their business logic between iOS and Android, no one had ever undertaken the effort to enable building the entire application stack — from the low-level networking and persistence all the way up to the user interface — using just the Swift language.</p>
<p>But the time was right. SwiftUI was just reaching maturity, and its declarative design was flexible enough to target not only the mobile phone form factor, but also to scale all the way up to the full desktop and all the way down to the smartwatch. Expanding SwiftUI’s architecture to the “other” mobile platform was a daunting engineering challenge, but it made perfect sense from the standpoint of facilitating the creation of a whole app using a single language.</p>
<p>Developers who have adopted Skip for their dual-platform app development have loved it. But there has always been an undercurrent of caution and reservation about the future of the project, especially from larger enterprises for whom the architecture decisions were central to their business’ future. As we’ve written in the past, the best-in-class apps that top the charts on both the App Store and Play Store are not written once, but twice: they are written first in Swift for iOS, and then they are re-written in Kotlin for Android. Despite being enormously laborious to coordinate and maintain, writing the app twice has always been considered the safe choice, not just because it enables optimal performance and a <em>truly</em> native user interface, but also because they are using the languages and APIs that are recommended and supported by the operating system vendors themselves. How could an independent project by a small team possibly offer the same guarantees of technological durability?</p>
<div><h3 id="enter-the-swift-android-workgroup">Enter the Swift Android Workgroup</h3></div>
<p>Such concerns have presented a challenge and barrier against the adoption of Skip for cross-platform app development since the beginning. And so we joined together with some other visionaries and founded the Swift on Android Community Working Group<sup><a href="#user-content-fn-commgroup" id="user-content-fnref-commgroup" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup> earlier this year. Our goal was to collaborate in harnessing and coordinating the energies of various developers and businesses that had each dabbled in using Swift in some way for their Android apps.</p>
<p>The workgroup had so much excitement behind it that a few months later, it was blessed by the Swift Platform Steering Group as an official workgroup<sup><a href="#user-content-fn-officialgroup" id="user-content-fnref-officialgroup" data-footnote-ref="" aria-describedby="footnote-label">2</a></sup>, which meant that we had the backing and support of the Swift community as a whole. This was <strong>huge</strong>: Swift on Android was no longer a niche interest for risk-taking startups and indie developers, but was going to evolve into a fully-supported platform for the Swift language.</p>
<p>Work began in earnest. Since last year, Skip has been using an unofficial preview build of the toolchain and native Swift SDK for Android to power our “Skip Fuse” mode<sup><a href="#user-content-fn-nativeswiftandroid" id="user-content-fnref-nativeswiftandroid" data-footnote-ref="" aria-describedby="footnote-label">3</a></sup>. Using this technology as a base — which had evolved over the years in a somewhat haphazard fashion — we began the long process of getting it in shape for official approval and release: cleanup, bug fixes, ripping out unsupported dependencies, harmonizing the structure with other Swift SDKs, packaging, quality control, and continuous integration.</p>
<div><h3 id="the-official-release">The Official Release</h3></div>
<p>The culmination of all these efforts has at last arrived! As announced on the swift.org blog<sup><a href="#user-content-fn-announcement" id="user-content-fnref-announcement" data-footnote-ref="" aria-describedby="footnote-label">4</a></sup>, we are now publishing the Swift SDK as an officially supported platform. The Swift SDK for Android is available alongside the Static Linux (Musl) and WebAssembly (Wasm) SDKs, and will be available in nightly snapshot releases throughout the Swift 6.3 release cycle.</p>
<p>As mentioned, Skip is currently using our own preview release build of this SDK for our native Skip Fuse mode. So switching over to this official SDK will be smooth and painless for our current customers. We anticipate that the final Swift 6.3 release will be the point where we include it by default in the Skip installation and setup instructions.</p>
<p>Note that this SDK is not just theoretical, but is in active use <em>today</em> in many Skip-powered applications. Our own Skip Showcase<sup><a href="#user-content-fn-showcase" id="user-content-fnref-showcase" data-footnote-ref="" aria-describedby="footnote-label">5</a></sup> app has been running using this SDK, and provides a comprehensive sample of what is possible when you combine native Swift with Skip’s SwiftUI implementation for Android.</p>
<div><h2 id="the-future-for-swift-on-android">The Future for Swift on Android</h2></div>
<p>Since we announced the availability of the Swift SDK for Android, there has been an explosion of interest in the project. Many heretofore skeptics are realizing that this is <em>real</em>, and are seeing that Swift is a viable choice as the one language for their entire application stack — on all platforms. No longer do developers need to make the agonizing choice between writing an application in two separate native languages, versus settling on an inefficient and alien language like JavaScript or Dart for their shared codebase.</p>
<p>For Skip itself, this development grants us an <em>enormous</em> amount of confidence-building support. <strong>Swift on Android is here to stay</strong>. And so even if Skip as a product were to somehow disappear tomorrow, any investment that is made in Swift for Android development would continue be a viable and supported path going forward. Swift on Android is available today, it has official backing by the Swift project, and it is here to stay. The future is bright!</p>
<div align="center">
<a href="https://assets.skip.dev/screens/swift-sdk-for-android-in-action-showcase.png" target="_blank"><img alt="Screenshot of Skip Showcase native app" src="https://assets.skip.dev/screens/swift-sdk-for-android-in-action-showcase.png"></a>
<br>
<a href="https://play.google.com/store/apps/details?id=org.appfair.app.Showcase"><img src="https://assets.skip.dev/badges/google-play-store.svg" alt="Download on the Google Play Store"></a>
<a href="https://apps.apple.com/us/app/skip-showcase/id6474885022"><img src="https://assets.skip.dev/badges/apple-app-store.svg" alt="Download on the Apple App Store"></a>
</div>
<section data-footnotes="">
<ol>
<li id="user-content-fn-commgroup">
<p>Swift on Android Working Group, Community Showcase, February 10, 2025: <a href="https://forums.swift.org/t/swift-on-android-working-group/77780" rel="nofollow" target="_blank">https://forums.swift.org/t/swift-on-android-working-group/77780<span> ↗</span></a> <a href="#user-content-fnref-commgroup" data-footnote-backref="" aria-label="Back to reference 1">↩</a></p>
</li>
<li id="user-content-fn-officialgroup">
<p>Announcing the Android Workgroup, June 25, 2025: <a href="https://forums.swift.org/t/announcing-the-android-workgroup/80666" rel="nofollow" target="_blank">https://forums.swift.org/t/announcing-the-android-workgroup/80666<span> ↗</span></a> <a href="#user-content-fnref-officialgroup" data-footnote-backref="" aria-label="Back to reference 2">↩</a></p>
</li>
<li id="user-content-fn-nativeswiftandroid">
<p>Native Swift on Android, Part 1: Setup, Compiling, Running, and Testing: <a href="https://skip.dev/blog/native-swift-on-android-1/">/blog/native-swift-on-android-1/</a> <a href="#user-content-fnref-nativeswiftandroid" data-footnote-backref="" aria-label="Back to reference 3">↩</a></p>
</li>
<li id="user-content-fn-announcement">
<p>Announcing the Swift SDK for Android: <a href="https://www.swift.org/blog/nightly-swift-sdk-for-android/" rel="nofollow" target="_blank">https://www.swift.org/blog/nightly-swift-sdk-for-android/<span> ↗</span></a> <a href="#user-content-fnref-announcement" data-footnote-backref="" aria-label="Back to reference 4">↩</a></p>
</li>
<li id="user-content-fn-showcase">
<p>Skip Showcase <a href="https://skip.dev/docs/samples/skipapp-showcase-fuse/">/docs/samples/skipapp-showcase-fuse/</a> <a href="#user-content-fnref-showcase" data-footnote-backref="" aria-label="Back to reference 5">↩</a></p>
</li>
</ol>
</section>swiftandroidsdkofficial-releaseswift-6.3workgroupnativecross-platformannouncementSkip Showcase: Securing your secrets with SkipKeychainhttps://skip.dev/blog/showcase-keychain/https://skip.dev/blog/showcase-keychain/Thu, 25 Sep 2025 00:00:00 GMT<p>Skip is a technology that enables the creation of dual-platform iOS/Android apps from a single shared Swift and SwiftUI codebase. The headline feature of Skip is that it takes your SwiftUI interface and automatically translates it into the equivalent Jetpack Compose interface, so your app is genuinely native on <em>both</em> platforms.</p>
<p>However, there is much more to Skip than just SwiftUI: there is a whole suite of optional modules that you can add to your project that provide additional integrations with the underlying platform. These are primarily distributed as free and open-source Swift Package Manager repositories under the <a href="http://github.com/skiptools" rel="nofollow" target="_blank">skiptools<span> ↗</span></a> GitHub organization, but many developers have also created their own. The equivalent package index in the Flutter world would be <a href="https://pub.dev" rel="nofollow" target="_blank">pub.dev<span> ↗</span></a>, and the closest thing for React might be <a href="https://expo.dev" rel="nofollow" target="_blank">expo.dev<span> ↗</span></a>.</p>
<p>This first post in the “Exploring Showcase” series will discuss the <a href="https://skip.dev/docs/modules/skip-keychain/">SkipKeychain</a> module, which provides a common API for accessing encrypted secrets on both iOS and Android. Future entries will cover other integrations, such as native embedded <a href="https://skip.dev/docs/modules/skip-web/">web views</a> and <a href="https://skip.dev/docs/modules/skip-av/">video players</a>, <a href="https://skip.dev/docs/modules/skip-motion/">Lottie animations</a>, <a href="https://skip.dev/docs/modules/skip-firebase/">Firebase</a> support, local <a href="https://skip.dev/docs/modules/skip-sql/">SQLite persistence</a>, <a href="https://skip.dev/docs/modules/skip-bluetooth/">Bluetooth</a> and <a href="https://skip.dev/docs/modules/skip-nfc/">NFC</a> hardware, and <a href="https://skip.dev/docs/modules/skip-device/">device sensor</a> access.</p>
<p>As with many of Skip’s features, we provide a demonstration in our eponymous “Showcase” app, which is available on both the Apple App Store and Google Play Store, and whose full source code is available in the <a href="https://github.com/skiptools/skipapp-showcase" rel="nofollow" target="_blank">skipapp-showcase<span> ↗</span></a> repository.</p>
<div align="center">
<a href="https://play.google.com/store/apps/details?id=org.appfair.app.Showcase"><img src="https://assets.skip.dev/badges/google-play-store.svg" alt="Download on the Google Play Store"></a>
<a href="https://apps.apple.com/us/app/skip-showcase/id6474885022"><img src="https://assets.skip.dev/badges/apple-app-store.svg" alt="Download on the Apple App Store"></a>
</div>
<div><h2 id="what-is-skipkeychain">What is SkipKeychain?</h2></div>
<p>The SkipKeychain module provides the ability to store and retrieve “secrets” on the device. These are small pieces of data, such as passwords, notes, and encryption keys, that need to be secured locally.</p>
<p>Apple offers a set of Keychain services<sup><a href="#user-content-fn-keychainservices" id="user-content-fnref-keychainservices" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup> APIs that provide access to their Keychain on iOS (as well as macOS and others). On Android, the <code dir="auto">EncryptedSharedPreferences</code> is a <a href="https://developer.android.com/jetpack" rel="nofollow" target="_blank">Jetpack<span> ↗</span></a> security library feature that encrypts key-value pairs before storing them in a <a href="https://developer.android.com/reference/kotlin/android/content/SharedPreferences" rel="nofollow" target="_blank">SharedPreferences<span> ↗</span></a> file.</p>
<p>SkipKeychain is a great example of the power of Skip’s integration capabilities, because it is so simple. The whole thing is implemented in a single ~300 line file, <a href="https://github.com/skiptools/skip-keychain/blob/main/Sources/SkipKeychain/SkipKeychain.swift" rel="nofollow" target="_blank">SkipKeychain.swift<span> ↗</span></a>. This is a <em>transpiled</em> module, but it is bridged to native Swift, so it can be used equally well from a Skip Lite or Skip Fuse app<sup><a href="#user-content-fn-fuselite" id="user-content-fnref-fuselite" data-footnote-ref="" aria-describedby="footnote-label">2</a></sup>.</p>
<p>The API surface is quite simple:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>import</span><span> SkipKeychain</span></div></div><div><div>
</div></div><div><div><span>let</span><span> keychain </span><span>=</span><span> Keychain.</span><span>shared</span></div></div><div><div>
</div></div><div><div><span>try</span><span> keychain.set(</span><span>"</span><span>value</span><span>"</span><span><span>, </span><span>forKey</span><span>: </span></span><span>"</span><span>key</span><span>"</span><span>)</span></div></div><div><div><span>assert</span><span>(</span><span><span>keychain.</span><span>string</span></span><span>(</span><span><span>forKey</span><span>: </span></span><span>"</span><span>key</span><span>"</span><span>)</span><span> </span><span>==</span><span> </span><span>"</span><span>value</span><span>"</span><span>)</span></div></div><div><div>
</div></div><div><div><span>try</span><span> keychain.</span><span>removeValue</span><span>(</span><span><span>forKey</span><span>: </span></span><span>"</span><span>key</span><span>"</span><span>)</span></div></div><div><div><span>assert</span><span>(</span><span><span>keychain.</span><span>string</span></span><span>(</span><span><span>forKey</span><span>: </span></span><span>"</span><span>key</span><span>"</span><span>)</span><span> </span><span>==</span><span> nil</span><span>)</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>The Showcase playground for the Keychain uses this API like so:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>import</span><span> SwiftUI</span></div></div><div><div><span>import</span><span> SkipKeychain</span></div></div><div><div>
</div></div><div><div><span>struct</span><span> KeychainPlayground: </span><span>View </span><span>{</span></div></div><div><div><span> </span><span>@State</span><span> </span><span>var</span><span> allKeys: [</span><span>String</span><span>] </span><span>=</span><span> []</span></div></div><div><div>
</div></div><div><div><span> </span><span>/// load all the keys from the keychain</span></div></div><div><div><span> </span><span>func</span><span> </span><span>loadKeys</span><span>()</span><span> {</span></div></div><div><div><span><span> </span></span><span>allKeys </span><span>=</span><span> ((</span><span>try</span><span>?</span><span> Keychain.</span><span>shared</span><span>.keys()) </span><span>??</span><span> []).</span><span>sorted</span><span>()</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div>
</div></div><div><div><span> </span><span>var</span><span> body: </span><span>some</span><span> View {</span></div></div><div><div><span> </span><span>List</span><span> {</span></div></div><div><div><span> </span><span>Section</span><span> {</span></div></div><div><div><span> </span><span>ForEach</span><span>(</span><span><span>allKeys, </span><span>id</span><span>: \.</span></span><span>self</span><span>) { key </span><span>in</span></div></div><div><div><span> </span><span>NavigationLink</span><span> {</span></div></div><div><div><span> </span><span>KeychainValueEditor</span><span>(</span><span><span>key</span><span>: key, </span><span>isNewKey</span><span>: </span></span><span>false</span><span>)</span></div></div><div><div><span><span> </span></span><span>} </span><span>label</span><span>: {</span></div></div><div><div><span> </span><span>Text</span><span>(</span><span>key</span><span>)</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>.</span><span>onDelete</span><span> { indices </span><span>in</span></div></div><div><div><span> </span><span>for</span><span> keyIndex </span><span>in</span><span> indices {</span></div></div><div><div><span> </span><span>try</span><span>?</span><span> Keychain.</span><span>shared</span><span>.</span><span>removeValue</span><span>(</span><span><span>forKey</span><span>: allKeys</span></span><span>[</span><span>keyIndex</span><span>])</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span> </span><span>loadKeys</span><span>()</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>.</span><span>onAppear</span><span> {</span></div></div><div><div><span> </span><span>loadKeys</span><span>()</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>The result is a very simple yet functional secret manager which behaves identically on both iOS and Android:</p>
<div align="center">
<video id="intro_video" autoplay muted loop playsinline>
<source src="https://assets.skip.dev/videos/skip-keychain.mov" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
<div><h2 id="modular-skip">Modular Skip</h2></div>
<p>One takeaway from this very simple and useful framework is just how simple it is to develop and iterate on, unlike the cumbersome bridging technologies needed by other cross-platform frameworks, which often force the developer to implement platform support across multiple separate projects with a variety of languages and build tools.</p>
<p>In contrast, Skip’s framework support is as simple as can be: a single standard Swift Package Manage project using the skipstone plugin. Kotlin and Java APIs for Android can be dropped right into <code dir="auto">#if SKIP</code> blocks, and the whole thing can be tested using SwiftPM’s built-in testing support. And using the framework is just as standard: it is a simple Swift Package Manager dependency, added like:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>let</span><span> </span><span>package</span><span> </span><span>=</span><span> </span><span>Package</span><span>(</span></div></div><div><div><span><span> </span></span><span>name</span><span>: </span><span>"</span><span>skipapp-showcase</span><span>"</span><span>,</span></div></div><div><div><span><span> </span></span><span>products</span><span>: [</span></div></div><div><div><span><span> </span></span><span>.</span><span>library</span><span>(</span><span><span>name</span><span>: </span></span><span>"</span><span>Showcase</span><span>"</span><span><span>, </span><span>type</span><span>: .</span><span>dynamic</span><span>, </span><span>targets</span><span>: [</span></span><span>"</span><span>Showcase</span><span>"</span><span>]</span><span>)</span><span>,</span></div></div><div><div><span><span> </span></span><span>],</span></div></div><div><div><span><span> </span></span><span>dependencies</span><span>: [</span></div></div><div><div><span><span> </span></span><span>.</span><span>package</span><span>(</span><span><span>url</span><span>: </span></span><span>"</span><span>https://source.skip.tools/skip.git</span><span>"</span><span><span>, </span><span>from</span><span>: </span></span><span>"</span><span>1.0.0</span><span>"</span><span>)</span><span>,</span></div></div><div><div><span><span> </span></span><span>.</span><span>package</span><span>(</span><span><span>url</span><span>: </span></span><span>"</span><span>https://source.skip.tools/skip-ui.git</span><span>"</span><span><span>, </span><span>from</span><span>: </span></span><span>"</span><span>1.0.0</span><span>"</span><span>)</span><span>,</span></div></div><div><div><span><span> </span></span><span>.</span><span>package</span><span>(</span><span><span>url</span><span>: </span></span><span>"</span><span>https://source.skip.tools/skip-keychain.git</span><span>"</span><span>, </span><span>"</span><span>0.3.0</span><span>"</span><span>..<</span><span>"</span><span>2.0.0</span><span>"</span><span>)</span><span>,</span></div></div><div><div><span><span> </span></span><span>],</span></div></div><div><div><span><span> </span></span><span>targets</span><span>: [</span></div></div><div><div><span><span> </span></span><span>.</span><span>target</span><span>(</span><span><span>name</span><span>: </span></span><span>"</span><span>Showcase</span><span>"</span><span><span>, </span><span>dependencies</span><span>: [</span></span></div></div><div><div><span><span> </span></span><span>.</span><span>product</span><span>(</span><span><span>name</span><span>: </span></span><span>"</span><span>SkipUI</span><span>"</span><span><span>, </span><span>package</span><span>: </span></span><span>"</span><span>skip-ui</span><span>"</span><span>)</span><span>,</span></div></div><div><div><span><span> </span></span><span>.</span><span>product</span><span>(</span><span><span>name</span><span>: </span></span><span>"</span><span>SkipKeychain</span><span>"</span><span><span>, </span><span>package</span><span>: </span></span><span>"</span><span>skip-keychain</span><span>"</span><span>)</span><span>,</span></div></div><div><div><span><span> </span></span><span>], </span><span>plugins</span><span>: [.</span><span>plugin</span><span>(</span><span><span>name</span><span>: </span></span><span>"</span><span>skipstone</span><span>"</span><span><span>, </span><span>package</span><span>: </span></span><span>"</span><span>skip</span><span>"</span><span>)</span><span>]</span><span>)</span><span>,</span></div></div><div><div><span><span> </span></span><span>]</span></div></div><div><div><span>)</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<div><h2 id="the-skip-advantage">The Skip Advantage</h2></div>
<p>The Skip philosophy is to take modern iOS-first development practices, and build on top of them to provide the ability to reach the entire marketplace. It does not add a whole new language and runtime on top of the platform, but instead enables <em>unintermediated</em> access to the platform using the native development language for each: Swift on iOS and Kotlin on Android. This results in the smallest app size possible, the most efficient performance, and the absolute best user experience possible, while still enabling the development of your app from a single codebase.</p>
<p>As always, <em>Happy Skipping</em>!</p>
<section data-footnotes="">
<ol>
<li id="user-content-fn-keychainservices">
<p>Keychain services: “Securely store small chunks of data on behalf of the user.” — <a href="https://developer.apple.com/documentation/security/keychain-services" rel="nofollow" target="_blank">https://developer.apple.com/documentation/security/keychain-services<span> ↗</span></a> <a href="#user-content-fnref-keychainservices" data-footnote-backref="" aria-label="Back to reference 1">↩</a></p>
</li>
<li id="user-content-fn-fuselite">
<p>For more on the distinction between transpiled Skip Lite and compiled Skip Fuse apps, see <a href="https://skip.dev/docs/status/">Skip Fuse vs. Lite</a>. <a href="#user-content-fnref-fuselite" data-footnote-backref="" aria-label="Back to reference 2">↩</a></p>
</li>
</ol>
</section>skip-keychainkeychainandroid-securityios-securitydata-protectioncross-platformskip-toolSkip on the Swift Package Indexing Podcasthttps://skip.dev/blog/skip-on-swift-package-indexing-podcast/https://skip.dev/blog/skip-on-swift-package-indexing-podcast/Thu, 28 Aug 2025 00:00:00 GMT<p>I was thrilled to be interviewed by Dave and Sven from the Swift Package Indexing podcast the other day! We talked about a wide range of topics around the founding of the <a href="https://www.swift.org/android-workgroup/" rel="nofollow" target="_blank">Swift Android Workgroup<span> ↗</span></a>, the progress that Swift is making in expanding into other platforms, and how Skip builds on the Swift Android SDK to enable building both iOS and Android apps from the same SwiftUI codebase.</p>
<p>We also discussed some tips for Swift package developers to make their own packages compatible with Android, and talked about the recent addition of Android compatibility to the supported platforms matrix for packages on <a href="https://swiftpackageindex.com/search?query=platform%3Aios%2Candroid" rel="nofollow" target="_blank">https://swiftpackageindex.com<span> ↗</span></a>.</p>
<p>You can check out <a href="https://swiftpackageindexing.transistor.fm/episodes/61-people-have-been-working-on-it-for-ten-years" rel="nofollow" target="_blank">Episode 61: “People have been working on it for ten years”<span> ↗</span></a> for the full transcript and show notes.</p>
<iframe width="100%" height="180" frameborder="no" scrolling="no" seamless src="https://share.transistor.fm/e/028e850b"></iframe>
<p>Peter and Sven are gracious hosts, and I greatly enjoyed our conversation.</p>skippodcastswift-android-workgroupswift-package-indexswiftandroidSkip and the next generation of mobile user interfaceshttps://skip.dev/blog/skip-next-gen-mobile-ui/https://skip.dev/blog/skip-next-gen-mobile-ui/Tue, 10 Jun 2025 00:00:00 GMT<aside aria-label="TL;DR"><p aria-hidden="true">TL;DR</p><div><p>Skip apps work with iOS’ new “Liquid Glass” design language from day one. Other cross-platform frameworks are not so lucky.</p></div></aside>
<p>When you write dual-platform Swift and SwiftUI apps with Skip, the user interface of your app is always truly native to the platform - on both iOS and Android. This means that your app’s widgets and navigation idioms will feel truly “at home” to all of your users, and all the accessibility features of the underlying operating system will automatically work. This is true regardless of whether you are using Skip Lite’s <a href="https://skip.dev/docs/modes/">transpiled mode</a> or Skip Fuse’s <a href="https://skip.dev/blog/fully-native-android-swift-apps/">more recent</a> natively-compiled Swift.</p>
<p>A platform-native user interface matters, not just visually and for performance reasons, but also because it keeps up with system changes without needing to play “catch up” when the underlying system’s frameworks are updated. As a case in point, this week’s unveiling of iOS 26’s new “Liquid Glass” user interface at the Apple Worldwide Developer Conference (WWDC) was followed by this exhortation about the importance of using native frameworks<sup><a href="#user-content-fn-nfwk" id="user-content-fnref-nfwk" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup>:</p>
<blockquote>
<p>When you use Apple’s native frameworks, you can write better apps with less code. Some other frameworks promise the ability to write code once for Android and iOS.<br><br>
And that may sound good, but by the time you’ve written custom code to adapt each platform’s conventions, connected to hardware with platform-specific APIs, implemented accessibility, and then filled in functionality gaps by adding additional logic and relying on a host of plugins, you’ve likely written a lot more code than you’d planned on.<br><br>
And you are still left with an app that could be slower, look out of place, and can’t directly take advantage of features like Live Activities and widgets. Apple’s frameworks are uncompromisingly focused on helping you build the best apps.</p>
</blockquote>
<p>We couldn’t agree more. Skip is uncompromisingly focused on helping you create the very best app experience using Apple’s frameworks on Apple devices, as well as the best experience using Android’s frameworks on Android devices. We describe Skip as a “dual-platform” technology rather than a “cross-platform” technology for a reason: we do not try to create our own lowest-common denominator imitation of the native experience. Rather, we let Apple be Apple and let Android be Android by embracing the platform-native interface and idioms that makes each operating system unique and beloved by their adherents.</p>
<p>This makes Skip unique among technologies that facilitate building universal apps from a single codebase. For example, shortly after the iOS interface redesign was previewed, an <a href="https://github.com/flutter/flutter/issues/170310" rel="nofollow" target="_blank">issue<span> ↗</span></a> was filed in the Flutter project by a contributor:</p>
<blockquote>
<p>With the introduction of iOS 26, Apple has begun rolling out the new Liquid Glass design language. This introduces significant changes to the visual styling and interaction behavior across native iOS apps. As a result, Flutter apps using the existing Cupertino widgets risk appearing visually outdated on the latest iOS devices, leading to a degraded user experience and a perception of apps being “non-native.”<br><br>
For developers targeting iOS users who expect modern, fluid design aesthetics, this represents a significant challenge. There is currently no way to adopt these design changes through Flutter’s existing Cupertino widget set.</p>
</blockquote>
<p>After some concerned discussion, the Flutter team issued a <a href="https://github.com/flutter/flutter/issues/170310#issuecomment-2959275864" rel="nofollow" target="_blank">proclamation<span> ↗</span></a>:</p>
<blockquote>
<p>As with Material 3 Expressive, we are not developing the new Apple’26 UI design features in the Cupertino library right now, and we will not be accepting contributions for these updates at this time.</p>
</blockquote>
<p>And with that statement, the door is closed on Flutter apps ever feeling genuinely native on future versions of either iOS or Android. A similar fate awaits any other technology that relies on mimicry to simulate the platform’s native user interface on iOS, such as Compose Multiplatform.</p>
<p>In contrast, Skip apps automatically work with the next generation of interface advances. Build and launch our sample <a href="https://skip.dev/docs/samples/skipapp-showcase/">Showcase app</a> an iOS 26 device or simulator and you will be presented with the new “Liquid Glass” interface automatically.</p>
<div>
<video autoplay muted loop playsinline poster="https://www.skip.tools/assets/video/skip-splash-poster.png">
<source src="https://www.skip.tools/assets/video/skip-liquid-glass.mov" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
<p>Similarly, Skip will allow you to opt into Material 3’s Expressive redesign as it matures, giving your Android users the latest iteration of the Material design language. Skip achieves this by doing precisely <em>nothing</em> on iOS, and by bridging your shared Swift and SwiftUI to the recommended system frameworks on Android. The result is a universal app that uses the native toolkits for each platform: SwiftUI on iOS, and Jetpack Compose on Android.</p>
<p>Whether you are contemplating building a brand new app or considering your options for the future of your existing app(s), we encourage you to consider the advantages of Skip’s philosophy. We summarize the benefits of Skip compared to other multi-platform app building technology on our <a href="https://skip.dev/compare/">comparison</a> page.</p>
<p>
You can follow us on Mastodon at
<a href="https://mas.to/@skiptools">https://mas.to/@skiptools</a>, and join in the Skip discussions at
<a href="http://forums.skip.dev/">http://forums.skip.dev/</a>. The Skip FAQ at
<a href="https://skip.dev/docs/faq/">/docs/faq/</a>
is there to answer any questions, and be sure to check out the video tours at
<a href="https://skip.dev/tour/">/tour/</a>. And, as always, you can reach out directly to us on our Slack channel at
<a href="https://skip.dev/slack/">/slack/</a>.
</p>
<section data-footnotes="">
<ol>
<li id="user-content-fn-nfwk">
<p>Matthew Firlik, Senior Director, Developer Relations at <a href="https://developer.apple.com/videos/play/wwdc2025/102/?time=2448" rel="nofollow" target="_blank">Platforms State of the Union<span> ↗</span></a> (timecode 40:50) <a href="#user-content-fnref-nfwk" data-footnote-backref="" aria-label="Back to reference 1">↩</a></p>
</li>
</ol>
</section>skipnative-uiios-26androidliquid-glassmaterial-3cross-platformFully Native Cross-Platform Swift Appshttps://skip.dev/blog/fully-native-android-swift-apps/https://skip.dev/blog/fully-native-android-swift-apps/Fri, 25 Apr 2025 00:00:00 GMT<img src="https://assets.skip.dev/images/skip-marketing-preview.jpg" alt="Screenshot">
<p>In our series on using native Swift on Android, we have covered the basics of the Swift-Android toolchain in <a href="https://skip.dev/blog/native-swift-on-android-1/">Part 1</a>, bridging between compiled Swift and Kotlin in <a href="https://skip.dev/blog/skip-native-tech-preview/">Part 2</a>, and the creation of a cross-platform app with a shared Swift model layer in <a href="https://skip.dev/blog/shared-swift-model/">Part 3</a>.</p>
<p>We are pleased to unveil the culmination of all of this work: Skip 1.5 now has the ability to create a 100% native Swift and SwiftUI app for both iOS and Android! You can now enjoy the safety, efficiency, and expressiveness of pure Swift throughout your entire cross-platform app, including the ability to tap into the vast ecosystem of Swift packages to support your app development.</p>
<p>This blog post will go over the process of creating and developing a Swift cross-platform app, explore the underlying technologies, and discuss our current status and next steps.</p>
<aside aria-label="Note"><p aria-hidden="true">Note</p><div><p>Interested in following Swift-on-Android’s march towards official status? Follow the Android category at <a href="https://forums.swift.org/c/development/android/" rel="nofollow" target="_blank">swift.org<span> ↗</span></a> for Working Group meeting notes and general discussion.</p></div></aside>
<div><h3 id="getting-started">Getting Started</h3></div>
<p>Ensure that you are on a macOS 14+ machine with <a href="https://developer.apple.com/xcode" rel="nofollow" target="_blank">Xcode 16<span> ↗</span></a>, <a href="https://developer.android.com/studio" rel="nofollow" target="_blank">Android Studio 2025<span> ↗</span></a>, and <a href="https://brew.sh" rel="nofollow" target="_blank">Homebrew<span> ↗</span></a> installed.</p>
<p>First, create and launch an <a href="https://developer.android.com/studio/run/managing-avds" rel="nofollow" target="_blank">Android emulator<span> ↗</span></a> for testing.</p>
<p>Next, open Terminal and type the following commands to install Skip and the native Swift-Android toolchain.</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>$ brew install skiptools/skip/skip</span></div></div><div><div><span>$ skip upgrade</span></div></div><div><div><span>$ skip android sdk install</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>Verify that everything is working with an additional Terminal command:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>$ skip checkup --native</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<aside aria-label="Caution"><p aria-hidden="true">Caution</p><div><p>If you haven’t used Skip before, this command may take a long time to complete! Skip has to download and install all of the libraries necessary for Android app development. If any steps in the checkup command fail, consult the generated log file, which should contain an error message describing the failure. You can seek assistance on our <a href="https://skip.dev/slack/">Slack</a> or <a href="https://forums.skip.dev" rel="nofollow" target="_blank">discussion forums<span> ↗</span></a>.</p></div></aside>
<p>You’re now ready to create your first fully native cross-platform Swift app:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>$ skip init --native-app --open-xcode --appid=bundle.id.HowdySkip howdy-skip HowdySkip</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>Assuming the app initialized successfully, your project will open in Xcode. Run the app against an iPhone simulator destination (the first build may take some time) and your app will launch on both the iOS simulator and the running Android emulator at the same time!</p>
<div><h3 id="the-app-template">The App Template</h3></div>
<p>The <code dir="auto">skip init</code> command creates a template app to get you started. It is a <code dir="auto">TabView</code> containing a “Welcome” view, a list of editable and re-orderable “Items”, and a “Settings” view:</p>
<img alt="Screenshot of the Howdy Swift native app Welcome Tab" src="https://assets.skip.dev/screens/skip-nativeapp-howdy-1.png">
<aside aria-label="Note"><p aria-hidden="true">Note</p><div><p>You can also browse the template app online in the <a href="https://github.com/skiptools/skipapp-howdy" rel="nofollow" target="_blank">Howdy Skip<span> ↗</span></a> repository.</p></div></aside>
<div><h4 id="contentviewswift">ContentView.swift</h4></div>
<p>Rather than importing <code dir="auto">SwiftUI</code>, you will notice that the <a href="https://github.com/skiptools/skipapp-howdy/blob/main/Sources/HowdySkip/ContentView.swift" rel="nofollow" target="_blank"><code dir="auto">Sources/HowdySkip/ContentView.swift</code><span> ↗</span></a> file instead imports <code dir="auto">SkipFuseUI</code>. When building for iOS, this simply redirects to a <code dir="auto">SwiftUI</code> import. Skip’s philosophy is to not intrude on the iOS side of the application: on Darwin platforms you are still using direct, non-intermediated SwiftUI, just as if you were building an app without Skip at all.</p>
<p>On Android, however, the <code dir="auto">SkipFuseUI</code> module bridges the SwiftUI API onto <a href="https://developer.android.com/compose" rel="nofollow" target="_blank">Jetpack Compose<span> ↗</span></a>, Android’s modern <a href="https://kotlinlang.org" rel="nofollow" target="_blank">Kotlin<span> ↗</span></a> UI toolkit. It does this through the intermediate <code dir="auto">SkipUI</code> module. Bridging to pure Jetpack Compose gives Android users a fully native user experience rather than the uncanny-valley replica generated by many cross-platform frameworks.</p>
<p><img src="https://assets.skip.dev/diagrams/skip-diagrams-skip-fuse-ui-bridge.svg" alt="Diagram of Skip's Swift-on-Android build process"></p>
<p>Consult the <code dir="auto">SkipUI</code> module’s <a href="https://skip.dev/docs/modules/skip-ui/#supported-swiftui">documentation</a> for a listing of currently-supported SwiftUI constructs on Android. You can also examine the <a href="https://github.com/skiptools/skipapp-showcase-fuse/tree/main/Sources/ShowcaseFuse" rel="nofollow" target="_blank">ShowcaseFuse<span> ↗</span></a> cross-platform sample app, which displays and exercises most supported SwiftUI components:</p>
<div>
<img src="https://assets.skip.dev/videos/showcase.gif" alt="Screen recording">
</div>
<aside aria-label="Tip"><p aria-hidden="true">Tip</p><div><p>Use <code dir="auto">#if os(Android)</code> conditions to customize your Swift and SwiftUI for Android, including excluding unsupported SwiftUI from your Android build. You don’t have to let an Android limitation affect your iOS experience! Later we’ll see how to augment your Android UI with pure Jetpack Compose.</p></div></aside>
<div><h4 id="viewmodelswift">ViewModel.swift</h4></div>
<p>The “Items” tab displays an editable list of items which are managed by <a href="https://github.com/skiptools/skipapp-howdy/blob/main/Sources/HowdySkip/ViewModel.swift" rel="nofollow" target="_blank"><code dir="auto">Sources/HowdySkip/ViewModel.swift</code><span> ↗</span></a>, which handles loading and saving the list of items to a simple JSON file. This code uses standard Foundation types (<code dir="auto">URL</code>, <code dir="auto">Data</code>, <code dir="auto">Date</code>, <code dir="auto">FileManager</code>, <code dir="auto">JSONEncoder</code>, <code dir="auto">JSONDecoder</code>, etc.) to handle the management and persistence of the items. Note that unlike the SwiftUI in <code dir="auto">ContentView</code>, none of this code is bridged into Kotlin: it is using the Apple <a href="http://github.com/apple/swift-foundation" rel="nofollow" target="_blank">swift-foundation<span> ↗</span></a> types directly, just as on iOS.</p>
<img alt="Screenshot of the Howdy Swift native app List Tab" src="https://assets.skip.dev/screens/skip-nativeapp-howdy-2.png">
<p>Despite only using native Foundation types, you will notice that <code dir="auto">ViewModel.swift</code> imports <code dir="auto">SkipFuse</code>. Just as <code dir="auto">SkipFuseUI</code> bridges your UI to Android, <code dir="auto">SkipFuse</code> bridges model-layer code. We use it here to enable our <code dir="auto">@Observable</code> view model to communicate changes to the Jetpack Compose user interface. This is discussed further in <a href="https://skip.dev/blog/skip-native-tech-preview/">Part 2</a> of the series. You generally don’t need to be concerned with the details other than to remember to <code dir="auto">import SkipFuse</code> (or <code dir="auto">SkipFuseUI</code>) any time you implement an <code dir="auto">@Observable</code>.</p>
<aside aria-label="Caution"><p aria-hidden="true">Caution</p><div><p>The Skip Xcode plugin will issue a warning if you define an <code dir="auto">@Observable</code> without importing <code dir="auto">SkipFuse</code> or <code dir="auto">SkipFuseUI</code>. The <code dir="auto">SkipFuse</code> module bridges other non-UI functionality as well, such as routing your <code dir="auto">OSLog.Logger</code> messages to Android’s standard <a href="https://developer.android.com/tools/logcat" rel="nofollow" target="_blank">Logcat<span> ↗</span></a> logging system.</p></div></aside>
<div><h4 id="bridging-into-kotlin-and-compose">Bridging into Kotlin and Compose</h4></div>
<p>The final tab of the sample app is the “Settings” screen. This exposes various settings and displays some information about the app. It also presents a little heart emoji, which is blue on iOS and green on Android.</p>
<img alt="Screenshot of the Howdy Swift native app Settings Tab" src="https://assets.skip.dev/screens/skip-nativeapp-howdy-3.png">
<p>We use the green heart emoji to demonstrate a powerful feature of Skip: the ability to embed code that directly calls Kotlin and Jetpack Compose APIs! Examining the <code dir="auto">SettingsView</code> in <a href="https://github.com/skiptools/skipapp-howdy/blob/main/Sources/HowdySkip/ContentView.swift#L158" rel="nofollow" target="_blank"><code dir="auto">ContentView.swift</code><span> ↗</span></a>, you will see the inclusion of a <code dir="auto">PlatformHeartView</code>, whose implementation looks like this:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>/// A view that shows a blue heart on iOS and a green heart on Android.</span></div></div><div><div><span>struct</span><span> PlatformHeartView : </span><span>View </span><span>{</span></div></div><div><div><span> </span><span>var</span><span> body: </span><span>some</span><span> View {</span></div></div><div><div><span><span> </span></span><span>#</span><span>if</span><span> !</span><span>os</span><span>(</span><span>Android</span><span>)</span></div></div><div><div><span> </span><span>Text</span><span>(</span><span><span>verbatim</span><span>: </span></span><span>"</span><span>💙</span><span>"</span><span>)</span></div></div><div><div><span><span> </span></span><span>#</span><span>else</span></div></div><div><div><span> </span><span>ComposeView</span><span> {</span></div></div><div><div><span> </span><span>HeartComposer</span><span>()</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>#</span><span>endif</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span>}</span></div></div><div><div>
</div></div><div><div><span>#</span><span>if</span><span> SKIP</span></div></div><div><div><span>/// Use a ContentComposer to integrate Compose content. This code will be transpiled to Kotlin.</span></div></div><div><div><span>struct</span><span> HeartComposer : </span><span>ContentComposer </span><span>{</span></div></div><div><div><span> </span><span>@Composable</span><span> </span><span>func</span><span> </span><span>Compose</span><span>(</span><span>context</span><span>: ComposeContext</span><span>)</span><span> {</span></div></div><div><div><span><span> </span></span><span>androidx.</span><span>compose</span><span>.</span><span>material3</span><span>.</span><span>Text</span><span>(</span><span>"</span><span>💚</span><span>"</span><span><span>, </span><span>modifier</span><span>: context.</span><span>modifier</span></span><span>)</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span>}</span></div></div><div><div><span>#</span><span>endif</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>What is going on here? Notice that on Android, we’re rendering the heart with a <code dir="auto">ComposeView</code>, a special SwiftUI view for including Jetpack Compose content in the form of a <code dir="auto">ContentComposer</code>.</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>/// Encapsulation of Composable content.</span></div></div><div><div><span>public</span><span> </span><span>protocol</span><span> ContentComposer {</span></div></div><div><div><span> </span><span>@Composable</span><span> </span><span>func</span><span> </span><span>Compose</span><span>(</span><span>context</span><span>: ComposeContext</span><span>)</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>But how can we define <code dir="auto">@Composable</code> functions and call Android APIs from within our native Swift code?</p>
<p>The magic lies in Skip’s ability to <em>transpile</em> Swift code into Kotlin, and <code dir="auto">SkipFuse</code>’s ability to bridge between Kotlin and your compiled Swift. Any code in a <code dir="auto">#if SKIP</code> block will be transformed into the equivalent Kotlin, so it is free to call other Kotlin and Java APIs directly. The generated <code dir="auto">#if SKIP</code> Kotlin will also be automatically <a href="https://skip.dev/docs/modes/#bridging">bridged</a> so that you can call it from your native Swift.</p>
<p><img src="https://assets.skip.dev/diagrams/skip-diagrams-skip-fuse-ui-custom.svg" alt="Diagram of Skip's custom Compose view embedding"></p>
<aside aria-label="Note"><p aria-hidden="true">Note</p><div><p><code dir="auto">ComposeView</code> enables you to easily mix custom Compose views into your app - whether by preference or to work around any Skip limitations. For a less contrived use case, see how the <a href="https://github.com/skiptools/skipapp-bookings-fuse/blob/main/Sources/TravelBookingsFuse/MapView.swift" rel="nofollow" target="_blank"><code dir="auto">MapView</code><span> ↗</span></a> in our TravelBookings sample app displays a <code dir="auto">GoogleMap</code> Composable.</p></div></aside>
<div>
<img src="https://assets.skip.dev/videos/mapview.gif" alt="Screen recording">
</div>
<div><h3 id="ecosystem-access">Ecosystem Access</h3></div>
<p>The ability to effectively embed Kotlin code is immensely powerful. It not only provides direct access Jetpack Compose and the Android SDK, but also enables you to tap into the complete Android ecosystem of libraries through Kotlin and Java <a href="https://skip.dev/docs/dependencies/">dependencies</a>.</p>
<p>Of course, using native Swift allows you to take advantage of the vast and growing ecosystem of available Swift packages as well. In our post on <a href="https://skip.dev/blog/android-native-swift-packages/">Bringing Swift Packages to Android</a> we introduced the <a href="https://swift-everywhere.org" rel="nofollow" target="_blank">swift-everywhere.org<span> ↗</span></a> site that tracks packages that are successfully building for Android. Since that post, community members have been implementing Android support, bringing the number of known packages that can be used on Android to 2,240! Popular projects like <a href="https://github.com/Alamofire/Alamofire.git" rel="nofollow" target="_blank">Alamofire<span> ↗</span></a>, <a href="https://github.com/google/flatbuffers.git" rel="nofollow" target="_blank">flatbuffers<span> ↗</span></a>, <a href="https://github.com/scinfu/SwiftSoup.git" rel="nofollow" target="_blank">SwiftSoup<span> ↗</span></a>, <a href="https://github.com/apple/swift-protobuf.git" rel="nofollow" target="_blank">swift-protobuf<span> ↗</span></a>, and <a href="https://github.com/skiptools/swift-sqlcipher.git" rel="nofollow" target="_blank">swift-sqlcipher<span> ↗</span></a> (just to name a few) can be added directly to your app and used in the same way on both iOS and Android.</p>
<p>Skip’s unique ability to directly call both Swift and Kotlin/Java APIs separates it from most cross-platform development frameworks, where integrating with the host system often requires bespoke adapters and extra layers of indirection.</p>
<div><h3 id="skip-showcase">Skip Showcase</h3></div>
<p>As a demonstration and validation of this technology, we have published one of our sample apps, <a href="https://github.com/skiptools/skipapp-showcase-fuse" rel="nofollow" target="_blank">Skip Showcase<span> ↗</span></a>, to both the Google Play Store and Apple App Store. This fully native Swift app demonstrates parity between SwiftUI components on iOS and Android.</p>
<div align="center">
<a href="https://play.google.com/store/apps/details?id=org.appfair.app.Showcase"><img src="https://appfair.org/assets/badges/google-play-store.svg" alt="Download on the Google Play Store"></a>
<a href="https://apps.apple.com/us/app/skip-showcase/id6474885022"><img src="https://appfair.org/assets/badges/apple-app-store.svg" alt="Download on the Apple App Store"></a>
</div>
<div><h3 id="status-and-next-steps">Status and Next Steps</h3></div>
<p>Despite being generally available, Skip’s native support is currently a technology <em>preview</em>. We are working on updating our documentation, finding and squashing remaining bugs and limitations, reducing build times, and generating smaller Android binaries. Even as a preview, however, you can build complete, production-ready cross-platform Swift apps, as Skip Notes demonstrates.</p>
<aside aria-label="Note"><p aria-hidden="true">Note</p><div><p>As always, you can seek assistance on our <a href="https://skip.dev/slack/">Slack</a> or <a href="https://forums.skip.dev" rel="nofollow" target="_blank">discussion forums<span> ↗</span></a>.</p></div></aside>
<p>Cross-platform Swift and SwiftUI have been a dream of ours for a long time. We are immensely excited about the possibilities this unlocks for creating truly best-in-class apps for both iOS and Android from a single codebase!</p>swiftswiftuiandroidioscross-platformnativejetpack-composekotlinmobile-developmentskipfuseSwift Everywhere: Bringing Swift Packages to Androidhttps://skip.dev/blog/android-native-swift-packages/https://skip.dev/blog/android-native-swift-packages/Mon, 20 Jan 2025 00:00:00 GMT<p><img src="https://assets.skip.dev/images/Swift-Android.svg" alt="Swift Android Logo">
{: style=“text-align: center; width: 200px; margin: auto;”}</p>
<ul>
<li>Table of contents
{:toc}</li>
</ul>
<div><h2 id="introduction">Introduction</h2></div>
<p>In recent weeks, the Skip team has submitted patches to numerous Swift projects to add Android support to their packages. We’ve been tracking the progress of Android build-ability on our <a href="https://swift-everywhere.org" rel="nofollow" target="_blank">swift-everywhere.org<span> ↗</span></a> web site, which catalogs a list of many popular Swift packages and whether they compile for Android. At the time of writing, nearly <strong>two thousand</strong> Swift packages are building successfully for Android, with more being added every day.</p>
<p>This article will go over what our experience porting Swift packages has taught us, and how you can apply this knowledge to turn your parochial Apple-only Swift package into a universal multi-platform package that can build for not just iOS and macOS, but also for Android.</p>
<aside aria-label="Tip"><p aria-hidden="true">Tip</p><div><p>To find out more about building full dual-platform iOS and Android apps with Swift, read our <a href="#next">ongoing blog series</a> on this topic.</p></div></aside>
<div><h2 id="package-prerequisites">Package Prerequisites</h2></div>
<p>What sorts of Swift packages are good candidates for Android? The best litmus test is whether the package offers <em>general-purpose</em> functionality, as opposed to having an integral dependency on iOS-specific frameworks. Some examples of good candidates are:</p>
<ul>
<li>Business logic</li>
<li>Algorithms and generic data structures</li>
<li>Networking utilities</li>
<li>Online web service and API clients</li>
<li>Data persistence</li>
<li>Parsers and formatters</li>
</ul>
<p>On the flip side, examples of packages that would be challenging to bring to Android might be:</p>
<ul>
<li>Custom UIKit components</li>
<li>HealthKit, CarPlay, Siri integration libraries</li>
<li>Other Apple-specific *Kit library integrations</li>
</ul>
<p>Now, just because a Swift package is designed to work with an Apple-specific framework doesn’t mean that it is impossible to port to Android. It just means that it would be a signifiant amount of work and involve creating a bridge to the equivalent Kotlin or Java framework. This is by all means possible – and will be the topic of a future post – but the subject of this article is how to bring <em>naturally portable</em> Swift packages to Android.</p>
<div><h2 id="setup-and-building">Setup and Building</h2></div>
<p>Say you have a <a href="https://developer.apple.com/documentation/xcode/swift-packages" rel="nofollow" target="_blank">conventional Swift package<span> ↗</span></a> that contains a <code dir="auto">Package.swift</code> file at the root and has the usual <code dir="auto">Sources/</code> and (hopefully) <code dir="auto">Tests/</code> folders that contain the individual targets and source files.</p>
<p>Does running <code dir="auto">swift build</code> and <code dir="auto">swift test</code> in the package directory work from the Terminal? If so, then you already have a cross-platform package that works on multiple platforms: iOS and macOS! That, by itself, is a good sign that your package might be suitable for Android. Many frameworks that are available on iOS are not present on macOS, so either your package doesn’t use too many iOS frameworks, or it is smart enough to only reference them conditionally (more on that below). But how do we build and test for Android?</p>
<p>First, install Skip and the native Android SDK by following the instructions in our <a href="https://skip.dev/docs/gettingstarted/">documentation</a>. Then try to build your Swift package with the Android toolchain. The very abbreviated quick start looks like:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>$ brew install skiptools/skip/skip</span></div></div><div><div>
</div></div><div><div><span>…</span></div></div><div><div><span>skip was successfully installed!</span></div></div><div><div>
</div></div><div><div><span>$ skip android sdk install</span></div></div><div><div>
</div></div><div><div><span>[✓] Install Swift Android SDK (2.4s)</span></div></div><div><div>
</div></div><div><div><span>$ cd MySwiftPackage/</span></div></div><div><div><span>$ skip android build</span></div></div><div><div>
</div></div><div><div><span>Building for debugging...</span></div></div><div><div><span>[0/2] Write sources</span></div></div><div><div><span>…</span></div></div><div><div><span>[4/4] Emitting module DemoPackage</span></div></div><div><div><span>Build complete! (1.85s)</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>If you see “Build complete!” then congratulations! Your package already builds for Android, and you can move on to the <a href="#testing">Testing</a> section. But if you encounter errors from the build command, you will need to <em>port</em> your package over to Android. Read on…</p>
<div><h2 id="porting-your-swift-package">Porting your Swift Package</h2></div>
<p>Wikipedia defines <a href="https://en.wikipedia.org/wiki/Porting" rel="nofollow" target="_blank">porting<span> ↗</span></a> as the “process of adapting software for the purpose of achieving some form of execution in a computing environment that is different from the one that a given program (meant for such execution) was originally designed for”.</p>
<p>In other words, you made your Swift package with iOS in mind, and now you want it to work on Android. The following sections will go over some of the most common issues you may hit when first trying to build your package on this new platform.</p>
<div><h3 id="conditionally-importing-and-using-platform-specific-modules">Conditionally Importing and Using Platform-Specific Modules</h3></div>
<p>Suppose your Swift package defines an <code dir="auto">Event</code> protocol with a simple default implementation:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>protocol</span><span> Event {</span></div></div><div><div><span> </span><span>var</span><span> dateRange: </span><span>Range</span><span><Date> { </span><span>get</span><span> }</span></div></div><div><div><span> </span><span>var</span><span> isConfirmed: </span><span>Bool</span><span> { </span><span>get</span><span> }</span></div></div><div><div><span>}</span></div></div><div><div>
</div></div><div><div><span>struct</span><span> SimpleEvent : </span><span>Hashable</span><span>, </span><span>Codable </span><span>{</span></div></div><div><div><span> </span><span>let</span><span> start, end</span><span>:</span><span> Date</span></div></div><div><div><span> </span><span>let</span><span> confirmed</span></div></div><div><div><span>}</span></div></div><div><div>
</div></div><div><div><span>extension</span><span> SimpleEvent : </span><span>Event </span><span>{</span></div></div><div><div><span> </span><span>var</span><span> dateRange: </span><span>Range</span><span><Date> { </span><span>self</span><span>.start</span><span>..<self</span><span>.end }</span></div></div><div><div><span> </span><span>var</span><span> isConfirmed: </span><span>Bool</span><span> { confirmed }</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>Your package also includes an extension to implement <code dir="auto">Event</code> using the iOS <a href="https://developer.apple.com/documentation/eventkit" rel="nofollow" target="_blank">EventKit<span> ↗</span></a> framework, like so:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>import</span><span> EventKit</span></div></div><div><div>
</div></div><div><div><span>extension</span><span> EKEvent : </span><span>Event </span><span>{</span></div></div><div><div><span> </span><span>var</span><span> dateRange: </span><span>Range</span><span><Date> { </span><span>self</span><span>.</span><span>startDate</span><span>..<self</span><span>.</span><span>endDate</span><span> }</span></div></div><div><div><span> </span><span>var</span><span> isConfirmed: </span><span>Bool</span><span> { </span><span>self</span><span>.</span><span>status</span><span> </span><span>==</span><span> .</span><span>confirmed</span><span> }</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>EventKit is an Apple-only framework, so when you try to build the package for Android, you will hit an error:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>$ skip android build</span></div></div><div><div>
</div></div><div><div><span><span> </span></span><span>7 | import EventKit</span></div></div><div><div><span><span> </span></span><span>| `- error: no such module 'EventKit'</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>The solution to this is simple: wrap any code that references the missing module in <code dir="auto">#if canImport(EventKit)</code>, which <em>conditionally</em> compiles the code <strong>only</strong> when the specified module is available:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>protocol</span><span> Event {</span></div></div><div><div><span> </span><span>var</span><span> dateRange: </span><span>Range</span><span><Date> { </span><span>get</span><span> }</span></div></div><div><div><span> </span><span>var</span><span> isConfirmed: </span><span>Bool</span><span> { </span><span>get</span><span> }</span></div></div><div><div><span>}</span></div></div><div><div>
</div></div><div><div><span>struct</span><span> SimpleEvent : </span><span>Hashable</span><span>, </span><span>Codable </span><span>{</span></div></div><div><div><span> </span><span>let</span><span> start, end</span><span>:</span><span> Date</span></div></div><div><div><span> </span><span>let</span><span> confirmed</span></div></div><div><div><span>}</span></div></div><div><div>
</div></div><div><div><span>extension</span><span> SimpleEvent : </span><span>Event </span><span>{</span></div></div><div><div><span> </span><span>var</span><span> dateRange: </span><span>Range</span><span><Date> { </span><span>self</span><span>.start</span><span>..<self</span><span>.end }</span></div></div><div><div><span> </span><span>var</span><span> isConfirmed: </span><span>Bool</span><span> { confirmed }</span></div></div><div><div><span>}</span></div></div><div><div>
</div></div><div><div><span>#</span><span>if</span><span> </span><span>canImport</span><span>(</span><span>EventKit</span><span>)</span></div></div><div><div><span>import</span><span> EventKit</span></div></div><div><div>
</div></div><div><div><span>extension</span><span> EKEvent : </span><span>Event </span><span>{</span></div></div><div><div><span> </span><span>var</span><span> dateRange: </span><span>Range</span><span><Date> { </span><span>self</span><span>.</span><span>startDate</span><span>..<self</span><span>.</span><span>endDate</span><span> }</span></div></div><div><div><span> </span><span>var</span><span> isConfirmed: </span><span>Bool</span><span> { </span><span>self</span><span>.</span><span>status</span><span> </span><span>==</span><span> .</span><span>confirmed</span><span> }</span></div></div><div><div><span>}</span></div></div><div><div><span>#</span><span>endif</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>Now you will have all the general functionality of the package available to Android, which you can adapt for any Android-specific data structures you may create in the future.</p>
<aside aria-label="Tip"><p aria-hidden="true">Tip</p><div><p>Using <code dir="auto">canImport</code> is the single most useful tool in your porting toolkit. With it, you can seamlessly exclude code that is unsupported by your target platform, without needing to drastically restructure your package or split it into multiple modules. There are other directives that enable you to segment your code by target, operating system, and module availability. Read more about it at <a href="https://developer.apple.com/documentation/xcode/running-code-on-a-specific-version/" rel="nofollow" target="_blank">Running code on a specific platform or OS version<span> ↗</span></a>.</p></div></aside>
<div><h3 id="importing-the-foundation-sub-modules">Importing the Foundation Sub-Modules</h3></div>
<p>Consider the following simple utility that fetches a <code dir="auto">URL</code> and decodes it into an <code dir="auto">Item</code> struct:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>import</span><span> Foundation</span></div></div><div><div>
</div></div><div><div><span>struct</span><span> Item: </span><span>Decodable </span><span>{</span></div></div><div><div><span> </span><span>let</span><span> id: </span><span>Int</span></div></div><div><div><span> </span><span>let</span><span> name: </span><span>String</span></div></div><div><div><span>}</span></div></div><div><div>
</div></div><div><div><span>func</span><span> </span><span>fetch</span><span>(</span><span>_</span><span> </span><span>url</span><span>: URL</span><span>)</span><span> </span><span>async</span><span> </span><span>throws</span><span> </span><span>-></span><span> Item {</span></div></div><div><div><span> </span><span>let</span><span> </span><span>(</span><span>data, response</span><span>) </span><span>=</span><span> </span><span>try</span><span> </span><span>await</span><span> URLSession.</span><span>shared</span><span>.</span><span>data</span><span>(</span><span><span>from</span><span>: url</span></span><span>)</span></div></div><div><div><span> </span><span>return</span><span> </span><span>try</span><span> </span><span>JSONDecoder</span><span>().</span><span>decode</span><span>(</span><span>Item.</span><span>self</span><span><span>, </span><span>from</span><span>: data</span></span><span>)</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>You may be surprised to see this fail to compile for Android:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>$ skip android build</span></div></div><div><div>
</div></div><div><div><span>Building for debugging...</span></div></div><div><div><span>Fetcher.swift:9:49: error: type 'URLSession' (aka 'AnyObject') has no member 'shared'</span></div></div><div><div><span><span> </span></span><span>7 |</span></div></div><div><div><span><span> </span></span><span>8 | func fetch(_ url: URL) async throws -> Item {</span></div></div><div><div><span><span> </span></span><span>9 | let (data, response) = try await URLSession.shared.data(from: url)</span></div></div><div><div><span><span> </span></span><span>| `- error: type 'URLSession' (aka 'AnyObject') has no member 'shared'</span></div></div><div><div><span>10 | return try JSONDecoder().decode(Item.self, from: data)</span></div></div><div><div><span>11 | }</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>This somewhat confusing error message just means that it can’t find the <code dir="auto">URLSession</code> type, because it isn’t present in the <code dir="auto">Foundation</code> module on Android.</p>
<p>On Darwin platforms (macOS, iOS, and other Apple operating systems), the <code dir="auto">Foundation</code> module is an umbrella for a wide variety of functionality. But on other platforms, such as Android and Linux, <code dir="auto">Foundation</code> is broken up into multiple separate sub-components:</p>
<ul>
<li><code dir="auto">FoundationEssentials</code>: All the basic Foundation types: <code dir="auto">Date</code>, <code dir="auto">Calendar</code>, <code dir="auto">URL</code>, <code dir="auto">IndexSet</code>, etc.</li>
<li><code dir="auto">FoundationInternationalization</code>: <code dir="auto">DateFormatter</code>, <code dir="auto">NumberFormatter</code>, and other localization utilities</li>
<li><code dir="auto">FoundationNetworking</code>: <code dir="auto">URLSession</code>, <code dir="auto">URLCache</code>, and other networking utilities</li>
<li><code dir="auto">FoundationXML</code>: <code dir="auto">XMLParser</code></li>
</ul>
<aside aria-label="Note"><p aria-hidden="true">Note</p><div><p>To learn more about this architectural difference, check out the “What’s next for Foundation” post on the <a href="https://forums.swift.org/t/what-s-next-for-foundation/61939" rel="nofollow" target="_blank">Swift Forums<span> ↗</span></a>.</p></div></aside>
<p>The solution to this is simple: add a conditional import of <code dir="auto">FoundationNetworking</code> to any file that uses any networking functionality, like so:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>import</span><span> Foundation</span></div></div><div><div><span>#</span><span>if</span><span> </span><span>canImport</span><span>(</span><span>FoundationNetworking</span><span>)</span></div></div><div><div><span>import</span><span> FoundationNetworking</span></div></div><div><div><span>#</span><span>endif</span></div></div><div><div>
</div></div><div><div><span>struct</span><span> Item: </span><span>Decodable </span><span>{</span></div></div><div><div><span> </span><span>let</span><span> id: </span><span>Int</span></div></div><div><div><span> </span><span>let</span><span> name: </span><span>String</span></div></div><div><div><span>}</span></div></div><div><div>
</div></div><div><div><span>func</span><span> </span><span>fetch</span><span>(</span><span>_</span><span> </span><span>url</span><span>: URL</span><span>)</span><span> </span><span>async</span><span> </span><span>throws</span><span> </span><span>-></span><span> Item {</span></div></div><div><div><span> </span><span>let</span><span> </span><span>(</span><span>data, response</span><span>) </span><span>=</span><span> </span><span>try</span><span> </span><span>await</span><span> URLSession.</span><span>shared</span><span>.</span><span>data</span><span>(</span><span><span>from</span><span>: url</span></span><span>)</span></div></div><div><div><span> </span><span>return</span><span> </span><span>try</span><span> </span><span>JSONDecoder</span><span>().</span><span>decode</span><span>(</span><span>Item.</span><span>self</span><span><span>, </span><span>from</span><span>: data</span></span><span>)</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>This will include the required <code dir="auto">FoundationNetworking</code> module for platforms like Android and Linux that need it, but quietly ignore it on Darwin platforms like iOS and macOS where the networking types are included with the monolithic <code dir="auto">Foundation</code> framework.</p>
<div><h3 id="importing-the-android-module">Importing the Android Module</h3></div>
<p>Swift has excellent integration with C, and many useful functions come in from the system’s C library, which is called <code dir="auto">Darwin</code> on macOS and iOS. Take the simple example of calculating the hypotenuse of a triangle, which uses some math functions brought in from the standard C library:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>import</span><span> Darwin</span></div></div><div><div>
</div></div><div><div><span>func</span><span> </span><span>hypotenuse</span><span>(</span><span>a</span><span>: </span><span>Double</span><span>, </span><span>b</span><span>: </span><span>Double</span><span>)</span><span> </span><span>-></span><span> </span><span>Double</span><span> {</span></div></div><div><div><span> </span><span>return</span><span> </span><span>sqrt</span><span>(</span><span>pow</span><span>(</span><span>a, </span><span>2</span><span>)</span><span> </span><span>+</span><span><span> </span><span>pow</span></span><span>(</span><span>b, </span><span>2</span><span>))</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>If you try to build this for Android, you will hit the error:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>1 | import Darwin</span></div></div><div><div><span><span> </span></span><span>| `- error: no such module 'Darwin'</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>This is because the <code dir="auto">Darwin</code> module doesn’t exist for Android. It is instead simply called <code dir="auto">Android</code>. Again, we solve this with our handy conditional <code dir="auto">canImport</code>:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>#</span><span>if</span><span> </span><span>canImport</span><span>(</span><span>Darwin</span><span>)</span></div></div><div><div><span>import</span><span> Darwin</span></div></div><div><div><span>#</span><span>elseif</span><span> </span><span>canImport</span><span>(</span><span>Android</span><span>)</span></div></div><div><div><span>import</span><span> Android</span></div></div><div><div><span>#</span><span>else</span></div></div><div><div><span>#error</span><span>(</span><span>"</span><span>Unknown platform</span><span>"</span><span>)</span></div></div><div><div><span>#</span><span>endif</span></div></div><div><div>
</div></div><div><div><span>func</span><span> </span><span>hypotenuse</span><span>(</span><span>a</span><span>: </span><span>Double</span><span>, </span><span>b</span><span>: </span><span>Double</span><span>)</span><span> </span><span>-></span><span> </span><span>Double</span><span> {</span></div></div><div><div><span> </span><span>return</span><span> </span><span>sqrt</span><span>(</span><span>pow</span><span>(</span><span>a, </span><span>2</span><span>)</span><span> </span><span>+</span><span><span> </span><span>pow</span></span><span>(</span><span>b, </span><span>2</span><span>))</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>In this case, we import either <code dir="auto">Darwin</code> on iOS and macOS, or <code dir="auto">Android</code> on Android. Both of these will provide access to the system’s standard C library.</p>
<aside aria-label="Caution"><p aria-hidden="true">Caution</p><div><p>Some other code you might encounter will use a conditional import for <code dir="auto">Bionic</code> instead of <code dir="auto">Android</code>. The <code dir="auto">Android</code> module is a superset of the <code dir="auto">Bionic</code> Android C library, and is generally preferred unless you have some need to limit your imports to just those functions provided by <code dir="auto">Bionic</code>.</p></div></aside>
<aside aria-label="Danger"><p aria-hidden="true">Danger</p><div><p>Supporting platforms beyond Android would be an extension of this technique, with conditional imports for <code dir="auto">Glibc</code> (the dynamic Linux standard library) and <code dir="auto">Musl</code> (a static stdlib for Linux), as well as <code dir="auto">Windows</code> and <code dir="auto">WASI</code>. Expanding support to these platforms is beyond the scope of this article, but you can see a full example of the imports you might need in the <a href="https://github.com/apple/swift-log/blob/main/Sources/Logging/Locks.swift" rel="nofollow" target="_blank">swift-logging<span> ↗</span></a> project.</p></div></aside>
<div><h4 id="low-level-c-issues">Low-level C Issues</h4></div>
<p>Simple C functions (like <code dir="auto">pow</code> and <code dir="auto">sqrt</code>) will generally be surfaced in exactly the same way on Darwin and Android platforms. But the definition of some functions and data structures in the Android C library can sometimes differ in subtle ways. For example, the following code uses the <code dir="auto">FILE</code> type and <code dir="auto">fopen</code> and <code dir="auto">fwrite</code> C functions on Darwin platforms:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>import</span><span> Darwin</span></div></div><div><div>
</div></div><div><div><span>let</span><span> fd: </span><span>UnsafeMutablePointer</span><span><FILE> </span><span>=</span><span> </span><span>fopen</span><span>(</span><span>"</span><span>file.txt</span><span>"</span><span>, </span><span>"</span><span>w</span><span>"</span><span>)</span></div></div><div><div><span>var</span><span> buffer: [</span><span>UInt8</span><span>] </span><span>=</span><span> [</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>]</span></div></div><div><div><span>let</span><span> count: </span><span>Int</span><span> </span><span>=</span><span> buffer.</span><span>withUnsafeBufferPointer</span><span> { ptr </span><span>in</span></div></div><div><div><span> </span><span>fwrite</span><span>(</span><span><span>ptr.baseAddress, </span><span>MemoryLayout</span></span><span><</span><span>UInt8</span><span>></span><span>.stride, ptr.count, fd</span><span>)</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>This will fail to build for Android:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>$ skip android build</span></div></div><div><div>
</div></div><div><div><span>FileWrite.swift:15:30: error: cannot find type 'FILE' in scope</span></div></div><div><div><span>13 | #endif</span></div></div><div><div><span>14 |</span></div></div><div><div><span>15 | let fd: UnsafeMutablePointer<FILE> = fopen("file.txt", "w")</span></div></div><div><div><span><span> </span></span><span>| `- error: cannot find type 'FILE' in scope</span></div></div><div><div><span>16 | var buffer: [UInt8] = [1, 2, 3]</span></div></div><div><div><span>17 | let count: Int = buffer.withUnsafeBufferPointer { ptr in</span></div></div><div><div>
</div></div><div><div><span>FileWrite.swift:18:16: error: value of optional type 'UnsafePointer<UInt8>?' must be unwrapped to a value of type 'UnsafePointer<UInt8>'</span></div></div><div><div><span>16 | var buffer: [UInt8] = [1, 2, 3]</span></div></div><div><div><span>17 | let count: Int = buffer.withUnsafeBufferPointer { ptr in</span></div></div><div><div><span>18 | fwrite(ptr.baseAddress, MemoryLayout<UInt8>.stride, ptr.count, fd)</span></div></div><div><div><span><span> </span></span><span>| |- error: value of optional type 'UnsafePointer<UInt8>?' must be unwrapped to a value of type 'UnsafePointer<UInt8>'</span></div></div><div><div><span><span> </span></span><span>| |- note: coalesce using '??' to provide a default when the optional value contains 'nil'</span></div></div><div><div><span><span> </span></span><span>| `- note: force-unwrap using '!' to abort execution if the optional value contains 'nil'</span></div></div><div><div><span>19 | }</span></div></div><div><div><span>20 |</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>There are two separate issue here:</p>
<ul>
<li><code dir="auto">FILE</code> doesn’t exist on Android, so <code dir="auto">UnsafeMutablePointer<FILE></code> must be replaced with <code dir="auto">OpaquePointer</code></li>
<li>Functions like <code dir="auto">fwrite</code> that take a file pointer will not accept an optional, and so must be force-unwrapped from their pointer’s <code dir="auto">rawValue</code></li>
</ul>
<p>The following conditional typealias will handle the first issue, and simply force-unwrapping the pointer’s address (which should be valid on all platforms) addresses the second:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>#</span><span>if</span><span> </span><span>canImport</span><span>(</span><span>Darwin</span><span>)</span></div></div><div><div><span>import</span><span> Darwin</span></div></div><div><div><span>#</span><span>elseif</span><span> </span><span>canImport</span><span>(</span><span>Android</span><span>)</span></div></div><div><div><span>import</span><span> Android</span></div></div><div><div><span>#</span><span>else</span></div></div><div><div><span>#error</span><span>(</span><span>"</span><span>Unknown platform</span><span>"</span><span>)</span></div></div><div><div><span>#</span><span>endif</span></div></div><div><div>
</div></div><div><div><span>#</span><span>if</span><span> </span><span>os</span><span>(</span><span>Android</span><span>)</span></div></div><div><div><span>typealias</span><span> Descriptor </span><span>=</span><span> </span><span>OpaquePointer</span></div></div><div><div><span>#</span><span>else</span></div></div><div><div><span>typealias</span><span> Descriptor </span><span>=</span><span> </span><span>UnsafeMutablePointer</span><span><FILE></span></div></div><div><div><span>#</span><span>endif</span></div></div><div><div>
</div></div><div><div><span>let</span><span> fd: Descriptor </span><span>=</span><span> </span><span>fopen</span><span>(</span><span>"</span><span>file.txt</span><span>"</span><span>, </span><span>"</span><span>w</span><span>"</span><span>)</span></div></div><div><div><span>var</span><span> buffer: [</span><span>UInt8</span><span>] </span><span>=</span><span> [</span><span>1</span><span>, </span><span>2</span><span>, </span><span>3</span><span>]</span></div></div><div><div><span>let</span><span> count: </span><span>Int</span><span> </span><span>=</span><span> buffer.</span><span>withUnsafeBufferPointer</span><span> { ptr </span><span>in</span></div></div><div><div><span> </span><span>fwrite</span><span>(</span><span>ptr.baseAddress</span><span>!</span><span><span>, </span><span>MemoryLayout</span></span><span><</span><span>UInt8</span><span>></span><span>.stride, ptr.count, fd</span><span>)</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>Unless you are developing very low-level code that interfaces with the platform’s C library, you will rarely encounter these sorts of issues. But when you do, it is good to know that the solutions tend the be fairly simple. The most difficult part is often just deciphering the compilation failure message.</p>
<div><h2 id="testing">Testing</h2></div>
<p>So now your package builds for Android with the command: <code dir="auto">skip android build</code>. Amazing!</p>
<p>But you are only halfway there: you need to make sure your code not only builds for Android, but that it actually works. Hopefully, your Swift package includes test cases in the <code dir="auto">Test/</code> folder, and running the tests locally on your macOS machine with <code dir="auto">swift test</code> works. For example, with the <a href="https://github.com/apple/swift-algorithms" rel="nofollow" target="_blank"><code dir="auto">swift-algorithms</code><span> ↗</span></a> package:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>$ swift test</span></div></div><div><div><span>Building for debugging...</span></div></div><div><div><span>[78/78] Linking swift-algorithmsPackageTests</span></div></div><div><div><span>Build complete! (12.67s)</span></div></div><div><div>
</div></div><div><div><span>Test Suite 'All tests' started at 2025-01-21 19:25:03.841.</span></div></div><div><div><span>Test Suite 'swift-algorithmsPackageTests.xctest' started at 2025-01-21 19:25:03.842.</span></div></div><div><div><span>Test Suite 'AdjacentPairsTests' started at 2025-01-21 19:25:03.842.</span></div></div><div><div><span>Test Case '-[SwiftAlgorithmsTests.AdjacentPairsTests testEmptySequence]' started.</span></div></div><div><div><span>Test Case '-[SwiftAlgorithmsTests.AdjacentPairsTests testEmptySequence]' passed (0.002 seconds).</span></div></div><div><div><span>Test Case '-[SwiftAlgorithmsTests.AdjacentPairsTests testIndexTraversals]' started.</span></div></div><div><div><span>Test Case '-[SwiftAlgorithmsTests.AdjacentPairsTests testIndexTraversals]' passed (0.002 seconds).</span></div></div><div><div><span>…</span></div></div><div><div><span>Test Suite 'All tests' passed at 2025-01-21 19:25:05.718.</span></div></div><div><div><span><span> </span></span><span>Executed 212 tests, with 0 failures (0 unexpected) in 1.870 (1.876) seconds</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>In order to run your tests on Android, you will need to either plug in an Android device (with <a href="https://developer.android.com/studio/debug/dev-options#Enable-debugging" rel="nofollow" target="_blank">USB debugging<span> ↗</span></a> enabled), or else configure and launch an Android emulator, either from the command line or <a href="https://developer.android.com/studio/run/managing-avds" rel="nofollow" target="_blank">Android Studio<span> ↗</span></a>).</p>
<aside aria-label="Note"><p aria-hidden="true">Note</p><div><p>See <a href="https://skip.dev/blog/native-swift-on-android-1/#android-emulator-setup">Part 1</a> of this blog series for full details of getting started with the Android emulator.</p></div></aside>
<p>Once you have your Android development target setup, you can run your package’s test cases with the <code dir="auto">skip android test</code> command, which will compile the test cases, bundle them up (along with any associated resources), copy them to the Android device or emulator, and then execute the test cases remotely.</p>
<p>For example, for the <code dir="auto">swift-algorithms</code> package:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>% skip android test</span></div></div><div><div><span>[0/1] Planning build</span></div></div><div><div><span>Building for debugging...</span></div></div><div><div><span>…</span></div></div><div><div><span>[83/84] Linking swift-algorithmsPackageTests.xctest</span></div></div><div><div>
</div></div><div><div><span>Build complete! (11.68s)</span></div></div><div><div>
</div></div><div><div><span>[✓] Check Swift Package (0.87s)</span></div></div><div><div><span>[✓] Connecting to Android (0.18s)</span></div></div><div><div><span>[✓] Copying test files (0.88s)</span></div></div><div><div>
</div></div><div><div><span>Test Suite 'All tests' started at 2025-01-21 21:02:09.086</span></div></div><div><div><span>Test Suite 'swift-algorithms-1C77777B-CEC3-4075-8853-E77CECFCF30B.xctest' started at 2025-01-21 21:02:09.105</span></div></div><div><div><span>Test Suite 'AdjacentPairsTests' started at 2025-01-21 21:02:09.105</span></div></div><div><div><span>Test Case 'AdjacentPairsTests.testEmptySequence' started at 2025-01-21 21:02:09.105</span></div></div><div><div><span>Test Case 'AdjacentPairsTests.testEmptySequence' passed (0.014 seconds)</span></div></div><div><div><span>Test Case 'AdjacentPairsTests.testIndexTraversals' started at 2025-01-21 21:02:09.120</span></div></div><div><div><span>Test Case 'AdjacentPairsTests.testIndexTraversals' passed (0.004 seconds)</span></div></div><div><div><span>…</span></div></div><div><div><span>Test Suite 'All tests' passed at 2025-01-21 21:02:21.697</span></div></div><div><div><span><span> </span></span><span>Executed 212 tests, with 0 failures (0 unexpected) in 12.579 (12.579) seconds</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>If there are any test failures, this is where you will delve into the details of your test case, isolate the problem, and apply fixes. There are many reasons why tests may fail, such as assumptions about the filesystem layout. These will need to be examined and resolved on a case-by-case basis.</p>
<p>Once all your tests pass, you’ve successfully brought your Swift package to Android!</p>
<div><h3 id="bonus-continuous-integration">Bonus: Continuous Integration</h3></div>
<p>Once you have your package building and your tests passing, you will want to ensure that they <em>continue</em> to pass. Maintaining a package that supports multiple platforms can be more challenging than just a single platform, because often when a new feature is implemented or a bug is fixed, the change will only be tested on the platform the developer is currently working with. For example, if you are working on the iOS side of your application and make a bug fix in one of your packages, you may only test the changes on that one platform, but it may inadvertently break something on another platform.</p>
<p>This is where continuous integration (CI) can be useful. If you use GitHub as your package’s source code management system, you can utilize <a href="https://github.com/features/actions" rel="nofollow" target="_blank">GitHub Actions<span> ↗</span></a> to automatically build and test your package on multiple platforms whenever you push to the repository or, for example, whenever a pull request is created from a branch or fork.</p>
<p>In order to facilitate Android CI, we provide the <a href="https://github.com/marketplace/actions/swift-android-action" rel="nofollow" target="_blank">swift-android-action<span> ↗</span></a>, which enables you to build and test your package against Android in a single line of configuration.</p>
<p>The following example of a <code dir="auto">.github/workflows/ci.yml</code> script will build and test your package on each of macOS, iOS, Linux, and Android whenever a commit is pushed or a PR is created:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>name</span><span>: </span><span>swift package ci</span></div></div><div><div><span>on</span><span>:</span></div></div><div><div><span> </span><span>push</span><span>:</span></div></div><div><div><span> </span><span>branches</span><span>:</span></div></div><div><div><span><span> </span></span><span>- </span><span>'</span><span>*</span><span>'</span></div></div><div><div><span> </span><span>workflow_dispatch</span><span>:</span></div></div><div><div><span> </span><span>pull_request</span><span>:</span></div></div><div><div><span> </span><span>branches</span><span>:</span></div></div><div><div><span><span> </span></span><span>- </span><span>'</span><span>*</span><span>'</span></div></div><div><div><span>jobs</span><span>:</span></div></div><div><div><span> </span><span>linux-android</span><span>:</span></div></div><div><div><span> </span><span>runs-on</span><span>: </span><span>ubuntu-latest</span></div></div><div><div><span> </span><span>steps</span><span>:</span></div></div><div><div><span><span> </span></span><span>- </span><span>uses</span><span>: </span><span>actions/checkout@v4</span></div></div><div><div><span><span> </span></span><span>- </span><span>name</span><span>: </span><span>"</span><span>Test Swift Package on Linux</span><span>"</span></div></div><div><div><span> </span><span>run</span><span>: </span><span>swift test</span></div></div><div><div><span><span> </span></span><span>- </span><span>name</span><span>: </span><span>"</span><span>Test Swift Package on Android</span><span>"</span></div></div><div><div><span> </span><span>uses</span><span>: </span><span>skiptools/swift-android-action@v2</span></div></div><div><div><span> </span><span>macos-ios</span><span>:</span></div></div><div><div><span> </span><span>runs-on</span><span>: </span><span>macos-latest</span></div></div><div><div><span> </span><span>steps</span><span>:</span></div></div><div><div><span><span> </span></span><span>- </span><span>uses</span><span>: </span><span>actions/checkout@v4</span></div></div><div><div><span><span> </span></span><span>- </span><span>name</span><span>: </span><span>"</span><span>Test Swift Package on macOS</span><span>"</span></div></div><div><div><span> </span><span>run</span><span>: </span><span>swift test</span></div></div><div><div><span><span> </span></span><span>- </span><span>name</span><span>: </span><span>"</span><span>Test Swift Package on iOS</span><span>"</span></div></div><div><div><span> </span><span>run</span><span>: </span><span>xcodebuild test -sdk "iphonesimulator" -destination "platform=iOS Simulator,name=iPhone 15" -scheme "$(xcodebuild -list -json | jq -r '.workspace.schemes[-1]')"</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>You can see this workflow in play in many of the packages on GitHub that support Android, such as Skip’s own <a href="https://github.com/skiptools/swift-sqlcipher/actions" rel="nofollow" target="_blank">swift-sqlcipher<span> ↗</span></a> package.</p>
<p>In this way, you can be assured that once you have done the hard work of getting your package working with Android, it continues to work on <strong>all</strong> your supported platforms.</p>
<div><h2 id="conclusion">Conclusion</h2></div>
<p>Expanding your Swift packages to support platforms beyond iOS may at first seem daunting, but using the advice from this article, you can follow a few simple steps that will put you on the right track:</p>
<ol>
<li>Setup Skip and the Swift Android SDK</li>
<li>Try to build your package with <code dir="auto">skip android build</code></li>
<li>Identify build errors and resolve them with conditional imports and by accommodating platform differences</li>
<li>Set up an Android emulator or device for testing</li>
<li>Test your package with <code dir="auto">skip android test</code></li>
<li>Identify test failures and resolve them on a case-by-case basis</li>
</ol>
<p>This is the sequence we have used to add Android support to dozens of popular Swift packages, such as <a href="https://github.com/GraphQLSwift/GraphQL/pull/161" rel="nofollow" target="_blank">GraphQL<span> ↗</span></a>, <a href="https://github.com/krzyzanowskim/CryptoSwift/pull/1065" rel="nofollow" target="_blank">CryptoSwift<span> ↗</span></a>, and <a href="https://github.com/mxcl/PromiseKit/pull/1352" rel="nofollow" target="_blank">PromiseKit<span> ↗</span></a>. With nearly 2,000 Swift packages currently building for Android, we feel the platform has achieved enough critical mass to make Swift an attractive language for many parts of your apps on both major mobile platforms: iOS <strong>and</strong> Android. And if you have a popular GitHub package that builds for Android, expect it to show up at <a href="https://swift-everywhere.org" rel="nofollow" target="_blank">swift-everywhere.org<span> ↗</span></a> in the near future!</p>
<div><h2 id="next">Native Swift on Android Series</h2></div>
<p>To learn more about running Swift on Android and how it integrates with Skip’s tools for creating dual-platform mobile apps, please see our ongoing blog series on the topic:</p>
<ul>
<li><a href="https://skip.dev/blog/native-swift-on-android-1/">Part 1: A native Swift toolchain for Android</a></li>
<li><a href="https://skip.dev/blog/skip-native-tech-preview/">Part 2: Your first native Swift Android app</a></li>
<li><a href="https://skip.dev/blog/shared-swift-model/">Part 3: Using a shared native Swift model to power separate SwiftUI iOS and Jetpack Compose Android apps</a></li>
<li>Coming soon: Bridging Kotlin and Java API for consumption by native Swift</li>
<li>Coming soon: Incorporating native Swift, C, and C++ dependencies into your cross-platform Swift apps</li>
</ul>swiftandroidcross-platformnativepackage-managerspmportingtoolchainmobile-developmentNative Swift on Android, Part 3: Sharing a Swift Model Layerhttps://skip.dev/blog/shared-swift-model/https://skip.dev/blog/shared-swift-model/Thu, 19 Dec 2024 00:00:00 GMT<p>Native Swift on Android, Part 3: Sharing a Swift Model Layer</p>
<div><h2 id="introduction">Introduction</h2></div>
<p>This is the third installment in our series exploring native Swift on Android. In <a href="https://skip.dev/blog/native-swift-on-android-1/">Part 1</a> we discuss bringing the Swift compiler and toolchain to Android. <a href="https://skip.dev/blog/skip-native-tech-preview/">Part 2</a> introduces Skip’s tooling and technology for <a href="https://skip.dev/docs/native/">Swift Android app development</a> and leads you through the creation of your first cross-platform Swift app.</p>
<aside aria-label="Note"><p aria-hidden="true">Note</p><div><p>If you haven’t already, we highly recommend reading <a href="https://skip.dev/blog/skip-native-tech-preview/">Part 2</a> to familiarize yourself with Swift-on-Android app development.</p></div></aside>
<p>The app we create in Part 2 uses a compiled Swift model layer and a shared SwiftUI interface, which Skip <a href="https://en.wikipedia.org/wiki/Source-to-source_compiler" rel="nofollow" target="_blank">transpiles<span> ↗</span></a> to Jetpack Compose on Android. The following diagram illustrates this dual-platform, single-codebase architecture:</p>
<div>
<p><img src="https://assets.skip.dev/diagrams/skip-diagrams-native-model.svg" alt="Skip Native Diagram"></p>
</div>
<p>In this article, by contrast, we create separate iOS and Android apps. The iOS app and shared model layer are written in Swift and SwiftUI using Xcode. The Android app is written in <a href="https://kotlinlang.org" rel="nofollow" target="_blank">Kotlin<span> ↗</span></a> and <a href="https://developer.android.com/compose" rel="nofollow" target="_blank">Jetpack Compose<span> ↗</span></a> using Android Studio, and it imports the compiled Swift model as a dependency. This structure allows you to reuse the lower layers of your app logic while fully embracing the standard IDEs and UI toolkits on each platform:</p>
<div>
<p><img src="https://assets.skip.dev/diagrams/skip-diagrams-native-twoapps.svg" alt="Skip Shared Model Diagram"></p>
</div>
<aside aria-label="Note"><p aria-hidden="true">Note</p><div><p>Thanks to Skip’s ability to <a href="https://skip.dev/docs/platformcustomization/#swiftui-and-compose">move fluidly between SwiftUI and Compose</a>, you could also choose to share <em>parts</em> of your SwiftUI interface between iOS and Android while maintaining the rest separately.</p></div></aside>
<div><h2 id="travelposters">TravelPosters</h2></div>
<img alt="Simulators displaying the same app on iPhone and Android" src="https://assets.skip.dev/screens/skip-native-travelposters-sims.png">
<p>Our sample apps in this installment are iOS and Android versions of <code dir="auto">TravelPosters</code>, a simple scrolling grid displaying posters of famous cities. Each poster displays the city’s name and current temperature. You can mark your favorites, and these favorites are remembered across app launches.</p>
<div><h2 id="the-shared-model">The Shared Model</h2></div>
<p>Our shared <code dir="auto">TravelPostersModel</code>, therefore, has the following responsibilities:</p>
<ul>
<li>Provide a list of cities. Each city must supply its name and a poster image URL.</li>
<li>Fetch the current temperature for each city.</li>
<li>Allow the addition and removal of cities from an observable set of favorites.</li>
<li>Persist and restore the set of favorites across uses of the app.</li>
</ul>
<p>And given that our model will power both iOS <em>and Android</em> apps, we should add the following table-stakes Android requirements:</p>
<ul>
<li>We must be able to access our Swift model API naturally in Kotlin, just as in Swift.</li>
<li>Our mutable set of favorites must be observable not only to SwiftUI state tracking, but to Jetpack Compose state tracking as well.</li>
</ul>
<p>Fortunately, Swift is more than up to the task of meeting our model’s general requirements, and Skip’s <em>SkipFuse</em> technology will handle transparently bridging it all to Kotlin and Compose!</p>
<div><h3 id="installing-skip">Installing Skip</h3></div>
<p>If you plan on following along and you haven’t already installed Skip, follow <a href="https://skip.dev/blog/skip-native-tech-preview/">Part 2’s installation instructions</a>. This will quickly get you up and running with Skip, its requirements, and the native Swift Android toolchain.</p>
<aside aria-label="Tip"><p aria-hidden="true">Tip</p><div><p>You can find the completed sample including iOS and Android apps at <a href="https://github.com/skiptools/skipapp-travelposters-native" rel="nofollow" target="_blank">https://github.com/skiptools/skipapp-travelposters-native<span> ↗</span></a>.</p></div></aside>
<div><h3 id="creating-the-model-package">Creating the Model Package</h3></div>
<p>As a good, modern citizen of the Swift ecosystem, Skip works atop <a href="https://www.swift.org/documentation/package-manager/" rel="nofollow" target="_blank">Swift Package Manager<span> ↗</span></a>. Our shared model will be a Swift package configured to use <code dir="auto">skipstone</code>, the Skip build plugin. You could create this package and configure its use of Skip by hand, but Skip provides tooling to help.</p>
<p>First, create the folder structure we’ll use to hold our shared model as well as our iOS and Android apps. You do not <em>have</em> to house your apps together, but this is the structure we’ll use in this article.</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>mkdir travelposters</span></div></div><div><div><span>cd travelposters</span></div></div><div><div><span>mkdir iOS</span></div></div><div><div><span>mkdir Android</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>Now use the <code dir="auto">skip</code> tool to create the shared model package:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>skip init --native-model travel-posters-model TravelPostersModel</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<img alt="Output of running skip init --native-model" src="https://assets.skip.dev/screens/skip-native-package-init.png">
<p>This command generates a <code dir="auto">travel-posters-model</code> SwiftPM package containing the <code dir="auto">TravelPostersModel</code> Swift module. The <code dir="auto">--native-model</code> option ensures that the module will already be configured to compile natively on Android, and to bridge its public API to Kotlin. Our particular needs, however, require a couple of additional steps.</p>
<ol>
<li>
<p>We know that parts of our model will be <code dir="auto">@Observable</code>. In order for <code dir="auto">@Observables</code> to work on Android, we need a dependency on <code dir="auto">skip-model</code>. Edit the generated <code dir="auto">Package.swift</code> to add it:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>...</span></div></div><div><div><span>let</span><span> </span><span>package</span><span> </span><span>=</span><span> </span><span>Package</span><span>(</span></div></div><div><div><span><span> </span></span><span>name</span><span>: </span><span>"</span><span>travel-posters-model</span><span>"</span><span>,</span></div></div><div><div><span> </span><span>...</span></div></div><div><div><span><span> </span></span><span>dependencies</span><span>:</span><span> [</span></div></div><div><div><span><span> </span></span><span>.</span><span>package</span><span>(</span><span><span>url</span><span>: </span></span><span>"</span><span>https://source.skip.tools/skip.git</span><span>"</span><span><span>, </span><span>from</span><span>: </span></span><span>"</span><span>1.2.0</span><span>"</span><span>)</span><span>,</span></div></div><div><div><span><span> </span></span><span>.</span><span>package</span><span>(</span><span><span>url</span><span>: </span></span><span>"</span><span>https://source.skip.tools/skip-model.git</span><span>"</span><span><span>, </span><span>from</span><span>: </span></span><span>"</span><span>1.0.0</span><span>"</span><span>)</span><span>, </span><span>// <-- Insert</span></div></div><div><div><span><span> </span></span><span>.</span><span>package</span><span>(</span><span><span>url</span><span>: </span></span><span>"</span><span>https://source.skip.tools/skip-fuse.git</span><span>"</span><span>, </span><span>"</span><span>0.0.0</span><span>"</span><span>..<</span><span>"</span><span>2.0.0</span><span>"</span><span>)</span></div></div><div><div><span><span> </span></span><span>],</span></div></div><div><div><span><span> </span></span><span>targets</span><span>: [</span></div></div><div><div><span><span> </span></span><span>.</span><span>target</span><span>(</span><span><span>name</span><span>: </span></span><span>"</span><span>TravelPostersModel</span><span>"</span><span>,</span></div></div><div><div><span><span> </span></span><span>dependencies</span><span>: [</span></div></div><div><div><span><span> </span></span><span>.</span><span>product</span><span>(</span><span><span>name</span><span>: </span></span><span>"</span><span>SkipFuse</span><span>"</span><span><span>, </span><span>package</span><span>: </span></span><span>"</span><span>skip-fuse</span><span>"</span><span>)</span><span>,</span></div></div><div><div><span><span> </span></span><span>.</span><span>product</span><span>(</span><span><span>name</span><span>: </span></span><span>"</span><span>SkipModel</span><span>"</span><span><span>, </span><span>package</span><span>: </span></span><span>"</span><span>skip-model</span><span>"</span><span>)</span><span> </span><span>// <-- Insert</span></div></div><div><div><span><span> </span></span><span>],</span></div></div><div><div><span><span> </span></span><span>plugins</span><span>: [.</span><span>plugin</span><span>(</span><span><span>name</span><span>: </span></span><span>"</span><span>skipstone</span><span>"</span><span><span>, </span><span>package</span><span>: </span></span><span>"</span><span>skip</span><span>"</span><span>)</span><span>]</span><span>)</span><span>,</span></div></div><div><div><span> </span><span>...</span></div></div><div><div><span><span> </span></span><span>]</span></div></div><div><div><span>)</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
</li>
<li>
<p>The <code dir="auto">--native-model</code> option we passed to <code dir="auto">skip init</code> will configure Skip to automatically bridge our model’s public API from compiled Swift to Android’s <a href="https://source.android.com/docs/core/runtime" rel="nofollow" target="_blank">ART<span> ↗</span></a> Java runtime. This is done through the <a href="https://skip.dev/docs/modes/#configuration"><code dir="auto">skip.yml</code></a> configuration file included in every Skip module. By default, however, Skip assumes that you’ll be bridging to <em>transpiled</em> Swift and SwiftUI code. Instead, we’ll be consuming the model from pure Kotlin, so we want to optimize the bridging for Kotlin compatibility. We do this by editing the <code dir="auto">Sources/TravelPostersModel/Skip/skip.yml</code> file to look like this:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>skip</span><span>:</span></div></div><div><div><span> </span><span>mode</span><span>: </span><span>'</span><span>native</span><span>'</span></div></div><div><div><span> </span><span>bridging</span><span>:</span></div></div><div><div><span> </span><span>enabled</span><span>: </span><span>true</span></div></div><div><div><span> </span><span>options</span><span>: </span><span>'</span><span>kotlincompat</span><span>'</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
</li>
</ol>
<p>You can read more about the magic of bridging in the <a href="https://skip.dev/docs/modes/#bridging">documentation</a>.</p>
<p>With these updates in place, we’re now ready to iterate on our shared Swift model code!</p>
<div><h3 id="exploring-the-code">Exploring the Code</h3></div>
<p>The beauty of cross-platform Swift code is how boring it is. You can browse our model’s complete content <a href="https://github.com/skiptools/skipapp-travelposters-native/tree/main/travel-posters-model/Sources/TravelPostersModel" rel="nofollow" target="_blank">on GitHub<span> ↗</span></a>, but it looks more or less exactly as you’d expect given the previously-enumerated requirements. It has some <code dir="auto">Codable</code> structs to represent cities and weather:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>public</span><span> </span><span>struct</span><span> City : </span><span>Identifiable</span><span>, </span><span>Codable </span><span>{</span></div></div><div><div><span> </span><span>public</span><span> </span><span>typealias</span><span> ID </span><span>=</span><span> </span><span>Int</span></div></div><div><div>
</div></div><div><div><span> </span><span>public</span><span> </span><span>let</span><span> id: ID</span></div></div><div><div><span> </span><span>public</span><span> </span><span>let</span><span> name: </span><span>String</span></div></div><div><div><span> </span><span>public</span><span> </span><span>let</span><span> imageURL: URL</span></div></div><div><div><span> </span><span>...</span></div></div><div><div><span>}</span></div></div><div><div>
</div></div><div><div><span>public</span><span> </span><span>struct</span><span> WeatherConditions : </span><span>Hashable</span><span>, </span><span>Codable </span><span>{</span></div></div><div><div><span> </span><span>public</span><span> </span><span>let</span><span> temperature: </span><span>Double</span><span> </span><span>// 16.2</span></div></div><div><div><span> </span><span>public</span><span> </span><span>let</span><span> windspeed: </span><span>Double</span><span> </span><span>// 16.6</span></div></div><div><div><span> </span><span>...</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<aside aria-label="Note"><p aria-hidden="true">Note</p><div><p>These structs are used in other Skip samples as well, so they contain more information than we strictly need for <code dir="auto">TravelPosters</code>.</p></div></aside>
<p>The model uses <code dir="auto">URLSession</code> and <code dir="auto">JSONDecoder</code> to fetch the current weather:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>public</span><span> </span><span>struct</span><span> Weather : </span><span>Hashable</span><span>, </span><span>Codable </span><span>{</span></div></div><div><div><span> </span><span>public</span><span> </span><span>let</span><span> latitude: </span><span>Double</span><span> </span><span>// e.g.: 42.36515</span></div></div><div><div><span> </span><span>public</span><span> </span><span>let</span><span> longitude: </span><span>Double</span><span> </span><span>// e.g.: -71.0618</span></div></div><div><div><span> </span><span>public</span><span> </span><span>let</span><span> time: </span><span>Double</span><span> </span><span>// e.g.: 0.6880760192871094</span></div></div><div><div><span> </span><span>...</span></div></div><div><div><span> </span><span>public</span><span> </span><span>let</span><span> conditions: WeatherConditions</span></div></div><div><div>
</div></div><div><div><span> </span><span>enum</span><span> CodingKeys: </span><span>String</span><span>, </span><span>CodingKey </span><span>{</span></div></div><div><div><span> </span><span>case</span><span> </span><span>latitude</span><span> </span><span>=</span><span> </span><span>"</span><span>latitude</span><span>"</span></div></div><div><div><span> </span><span>case</span><span> </span><span>longitude</span><span> </span><span>=</span><span> </span><span>"</span><span>longitude</span><span>"</span></div></div><div><div><span> </span><span>case</span><span> </span><span>time</span><span> </span><span>=</span><span> </span><span>"</span><span>generationtime_ms</span><span>"</span></div></div><div><div><span> </span><span>...</span></div></div><div><div><span> </span><span>case</span><span> </span><span>conditions</span><span> </span><span>=</span><span> </span><span>"</span><span>current_weather</span><span>"</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div>
</div></div><div><div><span> </span><span>public</span><span> </span><span>static</span><span> </span><span>func</span><span> </span><span>fetch</span><span>(</span><span>latitude</span><span>: </span><span>Double</span><span>, </span><span>longitude</span><span>: </span><span>Double</span><span>)</span><span> </span><span>async</span><span> </span><span>throws</span><span> </span><span>-></span><span> Weather {</span></div></div><div><div><span> </span><span>let</span><span> factor </span><span>=</span><span> </span><span>pow</span><span>(</span><span>10.0</span><span>, </span><span>4.0</span><span>) </span><span>// API expects a lat/lon rounded to 4 places</span></div></div><div><div><span> </span><span>let</span><span> lat </span><span>=</span><span> </span><span>Double</span><span>(</span><span>round</span><span>(</span><span>latitude </span><span>*</span><span> factor</span><span>)) </span><span>/</span><span> factor</span></div></div><div><div><span> </span><span>let</span><span> lon </span><span>=</span><span> </span><span>Double</span><span>(</span><span>round</span><span>(</span><span>longitude </span><span>*</span><span> factor</span><span>)) </span><span>/</span><span> factor</span></div></div><div><div><span> </span><span>let</span><span> url </span><span>=</span><span> </span><span>URL</span><span>(</span><span><span>string</span><span>: </span></span><span>"</span><span>https://api.open-meteo.com/v1/forecast?latitude=</span><span>\(</span><span>lat</span><span>)</span><span>&longitude=</span><span>\(</span><span>lon</span><span>)</span><span>&current_weather=true</span><span>"</span><span>)</span><span>!</span></div></div><div><div>
</div></div><div><div><span> </span><span>var</span><span> request </span><span>=</span><span> </span><span>URLRequest</span><span>(</span><span><span>url</span><span>: url</span></span><span>)</span></div></div><div><div><span><span> </span></span><span>request.</span><span>setValue</span><span>(</span><span>"</span><span>skipapp-sample</span><span>"</span><span><span>, </span><span>forHTTPHeaderField</span><span>: </span></span><span>"</span><span>User-Agent</span><span>"</span><span>)</span></div></div><div><div>
</div></div><div><div><span> </span><span>let</span><span> </span><span>(</span><span>data, response</span><span>) </span><span>=</span><span> </span><span>try</span><span> </span><span>await</span><span> URLSession.</span><span>shared</span><span>.</span><span>data</span><span>(</span><span><span>for</span><span>: request</span></span><span>)</span></div></div><div><div><span> </span><span>return</span><span> </span><span>try</span><span> </span><span>JSONDecoder</span><span>().</span><span>decode</span><span>(</span><span>Weather.</span><span>self</span><span><span>, </span><span>from</span><span>: data</span></span><span>)</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>And it includes an <code dir="auto">@Observable CityManager</code> to provide the list of cities and to persist favorites:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>@Observable</span><span> </span><span>public</span><span> </span><span>final</span><span> </span><span>class</span><span> CityManager {</span></div></div><div><div><span> </span><span>private</span><span> </span><span>static</span><span> </span><span>let</span><span> favoritesURL </span><span>=</span><span> URL.</span><span>applicationSupportDirectory</span><span>.</span><span>appendingPathComponent</span><span>(</span><span>"</span><span>favorites.json</span><span>"</span><span>)</span></div></div><div><div>
</div></div><div><div><span> </span><span>public</span><span> </span><span>static</span><span> </span><span>let</span><span> shared </span><span>=</span><span> </span><span>CityManager</span><span>()</span></div></div><div><div>
</div></div><div><div><span> </span><span>private</span><span> </span><span>init</span><span>()</span><span> {</span></div></div><div><div><span> </span><span>do</span><span> {</span></div></div><div><div><span> </span><span>self</span><span>.</span><span>allCities</span><span> </span><span>=</span><span> </span><span>try</span><span> </span><span>JSONDecoder</span><span>().</span><span>decode</span><span>(</span><span>[City].</span><span>self</span><span><span>, </span><span>from</span><span>: localCitiesJSON.</span><span>data</span></span><span>(</span><span><span>using</span><span>: .utf8</span></span><span>)</span><span>!</span><span>).</span><span>sorted</span><span> { c1, c2 </span><span>in</span></div></div><div><div><span><span> </span></span><span>c1.</span><span>name</span><span> </span><span><</span><span> c2.</span><span>name</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>} </span><span>catch</span><span> {</span></div></div><div><div><span><span> </span></span><span>logger.</span><span>log</span><span>(</span><span>"</span><span>error loading cities: </span><span>\(</span><span>error</span><span>)</span><span>"</span><span>)</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span> </span><span>do</span><span> {</span></div></div><div><div><span> </span><span>self</span><span>.</span><span>favoriteIDs</span><span> </span><span>=</span><span> </span><span>try</span><span> </span><span>JSONDecoder</span><span>().</span><span>decode</span><span>(</span><span><span>[City.</span><span>ID</span><span>].</span></span><span>self</span><span><span>, </span><span>from</span><span>: </span><span>Data</span></span><span>(</span><span><span>contentsOf</span><span>: </span></span><span>Self</span><span><span>.</span><span>favoritesURL</span></span><span>))</span></div></div><div><div><span><span> </span></span><span>logger.</span><span>log</span><span>(</span><span>"</span><span>loaded favorites: </span><span>\(</span><span>self</span><span>.</span><span>favoriteIDs</span><span>)</span><span>"</span><span>)</span></div></div><div><div><span><span> </span></span><span>} </span><span>catch</span><span> {</span></div></div><div><div><span><span> </span></span><span>logger.</span><span>log</span><span>(</span><span>"</span><span>error loading favorites: </span><span>\(</span><span>error</span><span>)</span><span>"</span><span>)</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div>
</div></div><div><div><span> </span><span>public</span><span> </span><span>var</span><span> allCities: [City] </span><span>=</span><span> []</span></div></div><div><div>
</div></div><div><div><span> </span><span>public</span><span> </span><span>var</span><span> favoriteIDs: [City.ID] </span><span>=</span><span> [] {</span></div></div><div><div><span> </span><span>didSet</span><span> {</span></div></div><div><div><span><span> </span></span><span>logger.</span><span>log</span><span>(</span><span>"</span><span>saving favorites: </span><span>\(</span><span>self</span><span>.</span><span>favoriteIDs</span><span>)</span><span>"</span><span>)</span></div></div><div><div><span> </span><span>do</span><span> {</span></div></div><div><div><span> </span><span>try</span><span> FileManager.</span><span>default</span><span>.</span><span>createDirectory</span><span>(</span><span><span>at</span><span>: </span></span><span>Self</span><span><span>.</span><span>favoritesURL</span><span>.</span><span>deletingLastPathComponent</span></span><span>()</span><span><span>, </span><span>withIntermediateDirectories</span><span>: </span></span><span>true</span><span>)</span></div></div><div><div><span> </span><span>try</span><span> </span><span>JSONEncoder</span><span>().</span><span>encode</span><span>(</span><span>favoriteIDs</span><span>).</span><span>write</span><span>(</span><span><span>to</span><span>: </span></span><span>Self</span><span><span>.</span><span>favoritesURL</span></span><span>)</span></div></div><div><div><span><span> </span></span><span>} </span><span>catch</span><span> {</span></div></div><div><div><span><span> </span></span><span>logger.</span><span>log</span><span>(</span><span>"</span><span>error saving favorites: </span><span>\(</span><span>error</span><span>)</span><span>"</span><span>)</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span>}</span></div></div><div><div>
</div></div><div><div><span>private</span><span> </span><span>let</span><span> localCitiesJSON </span><span>=</span><span> </span><span>"""</span></div></div><div><div><span>...</span></div></div><div><div><span>"""</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>While this code is generally pretty standard, it does contain a few concessions to the realities of current Swift support on Android:</p>
<ul>
<li>
<p>In files that create an <code dir="auto">OSLog.Logger</code> or that define an <code dir="auto">@Observable</code> type, we also <code dir="auto">import SkipFuse</code>. In fact, Skip will surface a build warning in Xcode if you attempt to define an <code dir="auto">@Observable</code> in a bridged file that doesn’t import the SkipFuse framework!</p>
<p><a href="https://skip.dev/docs/modules/skip-fuse/">SkipFuse</a> is an umbrella framework that “fuses” the Swift and Android worlds. It makes sure that your <code dir="auto">OSLog</code> messages are routed to Android’s Logcat logging service, that your <code dir="auto">@Observable</code> state is tracked by Jetpack Compose, and more - all without changes to your normal code path.</p>
</li>
<li>
<p>You may notice other unfamiliar <code dir="auto">import</code> patterns as well. For example, <code dir="auto">Foundation</code> on Linux and Android is divided into <code dir="auto">Foundation</code>, <code dir="auto">FoundationNetworking</code>, <code dir="auto">FoundationInternationalization</code>, and <code dir="auto">FoundationXML</code>. So in <a href="https://github.com/skiptools/skipapp-travelposters-native/blob/main/travel-posters-model/Sources/TravelPostersModel/Weather.swift" rel="nofollow" target="_blank"><code dir="auto">Weather.swift</code><span> ↗</span></a> where we use <code dir="auto">URLSession</code>, we have the following imports:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>import</span><span> Foundation</span></div></div><div><div><span>#</span><span>if</span><span> </span><span>canImport</span><span>(</span><span>FoundationNetworking</span><span>)</span></div></div><div><div><span>import</span><span> FoundationNetworking</span></div></div><div><div><span>#</span><span>endif</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
</li>
<li>
<p>Though we do not need them here, you may encounter <code dir="auto">#if os(Android)</code> checks to conditionalize code for Android or Darwin platforms in other Android-supporting codebases, just as you’ll often find <code dir="auto">#if os(macOS)</code> conditions in macOS-supporting codebases.</p>
</li>
<li>
<p>We’re loading our cities JSON from a static string, but more tyically you would load the contents from a resource. SkipFuse supports bundling Swift module resources as idiomatic <a href="https://developer.android.com/guide/topics/resources/providing-resources#OriginalFiles" rel="nofollow" target="_blank">Android assets<span> ↗</span></a>.</p>
</li>
<li>
<p>While many Swift packages like Apple’s <a href="https://github.com/apple/swift-algorithms" rel="nofollow" target="_blank">swift-algorithms<span> ↗</span></a> compile cleanly for Android out of the box, others will require minor changes, and still others - particularly those that tie into the hardware or use one of Apple’s many OS “Kits” - may never work on Android. Swift on Android is still in its infancy, and it will take time for developers to build and test their packages on this new-to-Swift platform.</p>
</li>
</ul>
<aside aria-label="Tip"><p aria-hidden="true">Tip</p><div><p>Just because a package isn’t yet available on Android doesn’t mean you can’t use it in your iOS build! To do so, append <code dir="auto">.when(platforms: [.iOS])</code> to the dependency in <code dir="auto">Package.swift</code>, conditionalize your call sites using <code dir="auto">#if !os(Android)</code>, and either omit the functionality from your Android app or find an alternate Android solution.</p></div></aside>
<p>You can read much more about both the advantages and the limitations of native Swift on Android in our <a href="https://skip.dev/docs/native/">full native Swift documentation</a>. For the most part, though, relax and enjoy coding with the full power and expressiveness of Swift!</p>
<div><h3 id="testing">Testing</h3></div>
<p>Due to limitations on build plugins, building the <code dir="auto">travel-posters-model</code> package in Xcode does <strong>not</strong> perform an Android build. It only builds for iOS. Rather, there are two simple ways to build for Android: use <code dir="auto">skip export</code> to create an Android library archive, which we explore <a href="#exporting">later</a> in this article, or run the unit tests.</p>
<p>Skip configures every native module with an extra unit test that builds the module for Android, transpiles your <code dir="auto">XCTests</code> to <code dir="auto">JUnit</code> tests, and runs them. Thus you’ll see two sets of results on every test run: first from <code dir="auto">XCTest</code> and then from <code dir="auto">JUnit</code> on Android. Frequently running your tests is a great way to catch both logic bugs and Android compilation errors early. Read more in the <a href="https://skip.dev/docs/porting/#testing">native testing documentation</a>.</p>
<aside aria-label="Caution"><p aria-hidden="true">Caution</p><div><p>You must perform your tests against macOS - not an iOS simulator - for Skip to be able to build and run for Android.</p></div></aside>
<div><h2 id="the-ios-app">The iOS App</h2></div>
<p>Because our model is a standard SwiftPM package, you incorporate and use it on iOS like any other package. We briefly outline the steps we took to create and configure our sample iOS app below. <em>Feel free to skip this section!</em></p>
<ol>
<li>
<p>Use Xcode to create a new Workspace in the <code dir="auto">travelposters</code> directory alongside the <code dir="auto">travel-posters-model</code> package.</p>
</li>
<li>
<p>Use Xcode to create a new App project in the <code dir="auto">travelposters/iOS</code> directory. Close the project after creating it, because we’re going to add it to our Workspace instead.</p>
<img alt="Creating a new app project in Xcode" src="https://assets.skip.dev/screens/skip-native-travelposters-iosapp.png">
</li>
<li>
<p>Add the <code dir="auto">travel-posters-model</code> package to your Workspace.</p>
</li>
<li>
<p>Add the <code dir="auto">iOS/TravelPosters/TravelPosters.xcodeproj</code> app to your Workspace.</p>
</li>
<li>
<p>Add a package dependency from the app to the <code dir="auto">travel-posters-model</code> local package.</p>
<img alt="Adding a package dependency in Xcode" src="https://assets.skip.dev/screens/skip-native-travelposters-iosmodel.png">
</li>
</ol>
<p>You can now use your Xcode Workspace to iterate on both the shared model package and your iOS app. Browse the complete iOS <code dir="auto">TravelPosters</code> app <a href="https://github.com/skiptools/skipapp-travelposters-native/tree/main/iOS/TravelPostersNative/TravelPostersNative" rel="nofollow" target="_blank">here<span> ↗</span></a>.</p>
<div><h2 id="the-android-app">The Android App</h2></div>
<p>We create our <code dir="auto">TravelPosters</code> Android app using Android Studio, starting with the “Empty Activity” template. Tell Android Studio to place the app in our <code dir="auto">travelposters/Android</code> folder.</p>
<img alt="Creating a new app project in Android Studio" src="https://assets.skip.dev/screens/skip-native-travelposters-androidapp.png">
<p>Next, make <code dir="auto">Android/lib</code>, <code dir="auto">Android/lib/debug</code>, and <code dir="auto">Android/lib/release</code> directories. This is where we’ll place our compiled Swift model and Skip libraries.</p>
<img alt="Creating directories for our compiled Swift model" src="https://assets.skip.dev/screens/skip-native-travelposters-lib.png">
<p>We must also configure our project to <em>use</em> the new <code dir="auto">lib</code> directories. Edit the <code dir="auto">app</code> module’s <code dir="auto">build.gradle.kts</code> file to add these and other necessary dependencies:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>..</span><span>.</span></div></div><div><div><span>dependencies</span><span> {</span></div></div><div><div><span> </span><span>..</span><span>.</span></div></div><div><div>
</div></div><div><div><span> </span><span>implementation</span><span>(</span><span>"org.jetbrains.kotlin:kotlin-reflect:2.1.0"</span><span>) </span><span>// For reflection used by Skip</span></div></div><div><div><span> </span><span>implementation</span><span>(</span><span>"io.coil-kt:coil-compose:2.7.0"</span><span>) </span><span>// For AsyncImage used to display posters</span></div></div><div><div>
</div></div><div><div><span> </span><span>debugImplementation</span><span>(</span><span>fileTree</span><span>(</span><span>mapOf</span><span>(</span></div></div><div><div><span> </span><span>"dir"</span><span> to </span><span>"../lib/debug"</span><span>,</span></div></div><div><div><span> </span><span>"include"</span><span> to </span><span>listOf</span><span>(</span><span>"*.aar"</span><span>, </span><span>"*.jar"</span><span>),</span></div></div><div><div><span> </span><span>"exclude"</span><span> to </span><span>listOf</span><span><String>()</span></div></div><div><div><span><span> </span></span><span>)))</span></div></div><div><div><span> </span><span>releaseImplementation</span><span>(</span><span>fileTree</span><span>(</span><span>mapOf</span><span>(</span></div></div><div><div><span> </span><span>"dir"</span><span> to </span><span>"../lib/release"</span><span>,</span></div></div><div><div><span> </span><span>"include"</span><span> to </span><span>listOf</span><span>(</span><span>"*.aar"</span><span>, </span><span>"*.jar"</span><span>),</span></div></div><div><div><span> </span><span>"exclude"</span><span> to </span><span>listOf</span><span><String>()</span></div></div><div><div><span><span> </span></span><span>)))</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>To prevent errors in the deployed app, include the following in <code dir="auto">build.gradle.kts</code> as well:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>android</span><span> {</span></div></div><div><div><span> </span><span>packaging</span><span> {</span></div></div><div><div><span> </span><span>jniLibs</span><span> {</span></div></div><div><div><span> </span><span>// doNotStrip is needed to prevent errors like: java.lang.UnsatisfiedLinkError: dlopen failed: empty/missing DT_HASH/DT_GNU_HASH in "/data/app/…/base.apk!/lib/arm64-v8a/libdispatch.so" (new hash type from the future?) (see: https://github.com/finagolfin/swift-android-sdk/issues/67)</span></div></div><div><div><span><span> </span></span><span>keepDebugSymbols.</span><span>add</span><span>(</span><span>"**/*.so"</span><span>)</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>Finally, our app needs internet access permissions to fetch weather and display remote images. Update its <code dir="auto">AndroidManifest.xml</code> file:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span><span><?</span><span>xml</span></span><span> version</span><span>=</span><span>"</span><span>1.0</span><span>"</span><span> encoding</span><span>=</span><span>"</span><span>utf-8</span><span>"</span><span>?></span></div></div><div><div><span><span><</span><span>manifest</span><span> </span></span><span>xmlns:android</span><span>=</span><span>"</span><span>http://schemas.android.com/apk/res/android</span><span>"</span></div></div><div><div><span> </span><span>xmlns:tools</span><span>=</span><span>"</span><span>http://schemas.android.com/tools</span><span>"</span><span>></span></div></div><div><div>
</div></div><div><div><span> </span><span><span><</span><span>uses-permission</span><span> </span></span><span>android:name</span><span>=</span><span>"</span><span>android.permission.INTERNET</span><span>"</span><span> /></span></div></div><div><div>
</div></div><div><div><span><span> </span></span><span>...</span></div></div><div><div><span><span></</span><span>manifest</span><span>></span></span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>Find the complete <code dir="auto">TravelPosters</code> Android app <a href="https://github.com/skiptools/skipapp-travelposters-native/tree/main/Android" rel="nofollow" target="_blank">here<span> ↗</span></a>. The next sections detail how to export our shared model to the Android app and how to use it from our Kotlin code.</p>
<div><h2 id="exporting">Exporting the Shared Model to Android</h2></div>
<p>We’ve configured our Android app to look in the <code dir="auto">Android/lib/debug</code> and <code dir="auto">Android/lib/release</code> folders for our model, but how do we populate these folders?</p>
<p>The <code dir="auto">skip export</code> command generates Android archives of a target Swift package and all of its dependencies. It has many options, which you can explore with <code dir="auto">skip export help</code>. The following Terminal command builds our <code dir="auto">travel-posters-model</code> and its dependencies for Android in debug mode and places the resulting <code dir="auto">.aar</code> library archives in the <code dir="auto">Android/lib/debug</code> directory:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>skip export --project travel-posters-model -d Android/lib/debug/ --debug</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>To generate release archives instead:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>skip export --project travel-posters-model -d Android/lib/release/ --release</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<aside aria-label="Note"><p aria-hidden="true">Note</p><div><p>Make sure to sync Android Studio whenever you update the libraries so that it picks up the latest changes.</p></div></aside>
<img alt="Syncing Android Studio" src="https://assets.skip.dev/screens/skip-native-travelposters-syncstudio.png">
<div><h3 id="automation">Automation</h3></div>
<p>There are many ways to automate this process, from simple scripting to git submodules to publishing the Android <code dir="auto">travel-posters-model</code> output to a local Maven repository. Use whatever system fits your team’s workflow best.</p>
<p>For example, to re-build and re-launch the app after making changes to the Swift code, you might run:</p>
<div><figure><figcaption><span></span></figcaption><pre><code><div><div><span>skip export --project travel-posters-model -d Android/lib/debug/ --debug</span></div></div><div><div><span>gradle -p Android installDebug</span></div></div><div><div><span>adb shell am start -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -n tools.skip.travelposters/tools.skip.travelposters.MainActivity</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<div><h2 id="using-the-shared-model-on-android">Using the Shared Model on Android</h2></div>
<p>Now that we’ve set up the Android app to depend on our shared Swift model, what is it like to actually <em>use</em> the model in Kotlin and Compose code? The answer is that - thanks to SkipFuse <a href="https://skip.dev/docs/modes/#bridging">bridging</a> - it’s surprisingly natural!</p>
<p>Before we dive into using our model, though, we have to make a single call in our Android app’s main <code dir="auto">Activity</code> to initialize integration. Skip has extended <code dir="auto">Foundation.ProcessInfo</code> for this purpose:</p>
<div><figure><figcaption><span>MainActivity.kt</span></figcaption><pre><code><div><div><span>..</span><span>.</span></div></div><div><div>
</div></div><div><div><span>class</span><span> MainActivity : ComponentActivity() {</span></div></div><div><div><span> </span><span>override</span><span> </span><span>fun</span><span> </span><span>onCreate</span><span>(savedInstanceState: Bundle?) {</span></div></div><div><div><span> </span><span>super</span><span>.</span><span>onCreate</span><span>(savedInstanceState)</span></div></div><div><div>
</div></div><div><div><span><span> </span></span><span>skip.foundation.ProcessInfo.</span><span>launch</span><span>(context </span><span>=</span><span> </span><span>this</span><span>) </span><span>// <-- INSERT</span></div></div><div><div>
</div></div><div><div><span> </span><span>enableEdgeToEdge</span><span>(statusBarStyle </span><span>=</span><span> SystemBarStyle.</span><span>dark</span><span>(Color.TRANSPARENT))</span></div></div><div><div><span> </span><span>setContent</span><span> {</span></div></div><div><div><span> </span><span>..</span><span>.</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div>
</div></div><div><div><span> </span><span>..</span><span>.</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>No additional changes to Android’s normal startup code path are needed.</p>
<p>This article is not a tutorial on using Jetpack Compose. Rather, we will focus on the places where our Android UI interacts with Swift, starting with the <code dir="auto">CityList</code> function for displaying the scrolling list of posters:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>..</span><span>.</span></div></div><div><div><span>import</span><span> travel.posters.model.CityManager </span><span>// <-- 1</span></div></div><div><div>
</div></div><div><div><span>@Composable</span></div></div><div><div><span>fun</span><span> </span><span>CityList</span><span>(</span><span>..</span><span>.) {</span></div></div><div><div><span> </span><span>val</span><span> cityManager </span><span>=</span><span> CityManager.shared </span><span>// <-- 2</span></div></div><div><div><span> </span><span>LazyVerticalGrid</span><span>(</span><span>..</span><span>.) {</span></div></div><div><div><span> </span><span>for</span><span> (city </span><span>in</span><span> cityManager.allCities) { </span><span>// <-- 3</span></div></div><div><div><span> </span><span>item</span><span> {</span></div></div><div><div><span> </span><span>CityPoster</span><span>(city, isFavorite </span><span>=</span><span> { cityManager.favoriteIDs.</span><span>contains</span><span>(city.id) }, setFavorite </span><span>=</span><span> { isFavorite </span><span>-></span></div></div><div><div><span> </span><span>// 4</span></div></div><div><div><span> </span><span>val</span><span> favoriteIDs </span><span>=</span><span> cityManager.favoriteIDs.</span><span>toMutableList</span><span>()</span></div></div><div><div><span> </span><span>if</span><span> (isFavorite </span><span>&&</span><span> </span><span>!</span><span>favoriteIDs.</span><span>contains</span><span>(city.id)) {</span></div></div><div><div><span><span> </span></span><span>favoriteIDs.</span><span>add</span><span>(city.id)</span></div></div><div><div><span><span> </span></span><span>cityManager.favoriteIDs </span><span>=</span><span> favoriteIDs</span></div></div><div><div><span><span> </span></span><span>} </span><span>else</span><span> </span><span>if</span><span> (</span><span>!</span><span>isFavorite) {</span></div></div><div><div><span><span> </span></span><span>favoriteIDs.</span><span>remove</span><span>(city.id)</span></div></div><div><div><span><span> </span></span><span>cityManager.favoriteIDs </span><span>=</span><span> favoriteIDs</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>})</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>We’ve annotated the code above with four numbered comments. Let’s explain each:</p>
<ol>
<li>Our <code dir="auto">TravelPostersModel</code> module is exposed to Kotlin in the <code dir="auto">travel.posters.model</code> package. Skip simply divides your CamelCase Swift module names into ”.”-separated Kotlin package names. Single-word packages are reserved in Kotlin, so if your module name consists of a single word, Skip appends “.module”. For example, module <code dir="auto">Util</code> turns into Kotlin package <code dir="auto">util.module</code>.</li>
<li>Your Swift types and API have equivalent names and signatures in Kotlin.</li>
<li>The Swift <code dir="auto">CityManager.allCities</code> property of type <code dir="auto">[City]</code> bridges to a Kotlin <code dir="auto">kotlin.collections.List<City></code>. Consult the <a href="https://skip.dev/docs/bridging/">bridging reference</a> to learn more about specific type mappings.</li>
<li>Here we’re performing standard Compose state hoisting to manage the favorites list. Notice that we simply update our model - we do not explicitly trigger a change to the UI. Like SwiftUI, Compose automatically reacts to change in observed state, and SkipFuse ensures that our <code dir="auto">@Observable CityManager</code> is fully and transparently integrated in Compose state tracking.</li>
</ol>
<p>Each item in the city list is a <code dir="auto">CityPoster</code>. Let’s examine that function as well:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>..</span><span>.</span></div></div><div><div><span>import</span><span> travel.posters.model.City</span></div></div><div><div><span>import</span><span> travel.posters.model.Weather</span></div></div><div><div>
</div></div><div><div><span>@Composable</span></div></div><div><div><span>fun</span><span> </span><span>CityPoster</span><span>(city: City, isFavorite: () </span><span>-></span><span> Boolean, setFavorite: (Boolean) -> Unit) {</span></div></div><div><div><span> </span><span>Box</span><span> {</span></div></div><div><div><span> </span><span>val</span><span> url </span><span>=</span><span> city.imageURL </span><span>// <-- 1</span></div></div><div><div><span> </span><span>AsyncImage</span><span>(</span><span>..</span><span>.)</span></div></div><div><div><span> </span><span>..</span><span>.</span></div></div><div><div><span> </span><span>Column</span><span>(</span><span>..</span><span>.) {</span></div></div><div><div><span> </span><span>Row</span><span>(</span><span>..</span><span>.) {</span></div></div><div><div><span> </span><span>..</span><span>.</span></div></div><div><div><span> </span><span>Icon</span><span>(imageVector </span><span>=</span><span> Icons.Filled.Star,</span></div></div><div><div><span><span> </span></span><span>modifier </span><span>=</span><span> Modifier.</span><span>clickable</span><span> {</span></div></div><div><div><span> </span><span>setFavorite</span><span>(</span><span>!</span><span>isFavoriteState.</span><span>value</span><span>)</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>)</span></div></div><div><div><span> </span><span>..</span><span>.</span></div></div><div><div><span> </span><span>Text</span><span>(text </span><span>=</span><span> city.name, </span><span>..</span><span>.)</span></div></div><div><div><span> </span><span>..</span><span>.</span></div></div><div><div><span> </span><span>Box</span><span> {</span></div></div><div><div><span> </span><span>..</span><span>.</span></div></div><div><div><span> </span><span>LaunchedEffect</span><span>(city.id, degrees) {</span></div></div><div><div><span> </span><span>try</span><span> { </span><span>// <-- 2</span></div></div><div><div><span> </span><span>val</span><span> c </span><span>=</span><span> Weather.</span><span>fetch</span><span>( </span><span>// <-- 3</span></div></div><div><div><span><span> </span></span><span>latitude </span><span>=</span><span> city.latitude,</span></div></div><div><div><span><span> </span></span><span>longitude </span><span>=</span><span> city.longitude</span></div></div><div><div><span><span> </span></span><span>).conditions.temperature</span></div></div><div><div><span> </span><span>..</span><span>.</span></div></div><div><div><span><span> </span></span><span>} </span><span>catch</span><span> (exception: Exception) {</span></div></div><div><div><span><span> </span></span><span>Log.</span><span>e</span><span>(</span><span>"TravelPosters"</span><span>, </span><span>"Error fetching weather: </span><span>$exception</span><span>"</span><span>)</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span> </span><span>..</span><span>.</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>Once again, we’ve added numbered comments to points of interest in the code above:</p>
<ol>
<li>In addition to bridging your own types as well as built-in types like numbers, strings, arrays, and dictionaries, SkipFuse translates common Foundation types like <code dir="auto">Data</code>, <code dir="auto">Date</code>, <code dir="auto">URL</code>, and <code dir="auto">UUID</code> to their Kotlin equivalents. In this case the <code dir="auto">City.imageURL</code> property of type <code dir="auto">Foundation.URL</code> maps to a <code dir="auto">java.net.URI</code>. Again, see the <a href="https://skip.dev/docs/bridging/">bridging reference</a> for details.</li>
<li>Our <code dir="auto">Weather.fetch</code> Swift function is marked <code dir="auto">throws</code>. If the native call produces an error, the bridged Kotlin call with throw a standard Kotlin exception.</li>
<li><code dir="auto">Weather.fetch</code> is an <code dir="auto">async</code> Swift function. Skip therefore generates a Kotlin <code dir="auto">suspend</code> function and integrates the call with Kotlin coroutines. Hence the use of a <code dir="auto">LaunchedEffect</code> in our Compose code.</li>
</ol>
<p>As you can see, you invoke your Swift APIs naturally in Kotlin - almost exactly as if they were written in Kotlin themselves! Swift custom types, built-in types, and common Foundation types all translate to Kotlin/Java equivalents, thrown errors cause Kotlin exceptions, async Swift functions use Kotlin coroutines, etc. The goal is that using a module written in Swift should be almost indistinguishable from using a package written in Kotlin.</p>
<aside aria-label="Note"><p aria-hidden="true">Note</p><div><p>Browse the complete Android Compose code <a href="https://github.com/skiptools/skipapp-travelposters-native/tree/main/Android/app/src/main/java/tools/skip/travelposters" rel="nofollow" target="_blank">on GitHub<span> ↗</span></a>.</p></div></aside>
<div><h2 id="next-steps">Next Steps</h2></div>
<p>If you haven’t already, check out <a href="https://skip.dev/blog/native-swift-on-android-1/">Part 1</a> and especially <a href="https://skip.dev/blog/skip-native-tech-preview/">Part 2</a> of this series.</p>
<p>If you’d like to learn much more about SkipFuse, bridging, and native Swift on Android, consider reading our <a href="https://skip.dev/docs/native/">Native Swift Tech Preview</a> documentation.</p>
<p>You may also be interested in the nascent <a href="https://github.com/swiftlang/swift-java" rel="nofollow" target="_blank">swift-java<span> ↗</span></a> project, which is designed to facilitate communication between server-side Swift and Java libraries. While that is a very different environment than Android apps interacting with modern Kotlin APIs, they do overlap, and you might find <code dir="auto">swift-java's</code> bridging approach useful. We anticipate that as it matures, this bridge and Skip’s native bridging will begin to align more closely in their techniques and implementation details.</p>
<div><h2 id="next">Native Swift on Android Series</h2></div>
<p>Additional posts in the native Swift on Android series:</p>
<ul>
<li><a href="https://skip.dev/blog/native-swift-on-android-1/">Part 1: A native Swift toolchain for Android</a></li>
<li><a href="https://skip.dev/blog/skip-native-tech-preview/">Part 2: Your first native Swift Android app</a></li>
<li>Part 3: Using a shared native Swift model to power separate SwiftUI iOS and Jetpack Compose Android apps</li>
<li>Coming soon: Bridging Kotlin and Java API for consumption by native Swift</li>
<li>Coming soon: Incorporating native Swift, C, and C++ dependencies into your cross-platform Swift apps</li>
</ul>
<div><h2 id="conclusion">Conclusion</h2></div>
<p>Many cross-platform solutions allow you to share code, but they typically come with serious downsides:</p>
<ul>
<li>Performance issues from the use of interpreters and/or complex runtimes (Javascript)</li>
<li>High memory watermarks and unpredictable hitches caused by garbage collection (Javascript, Kotlin)</li>
<li>Lack of transparent integration with SwiftUI and/or Compose state tracking (C/C++)</li>
<li>Portability and memory safety concerns (C/C++)</li>
</ul>
<p>Swift exhibits none of these problems. Its safety, efficiency, and expressiveness make it an ideal choice for cross-platform development. Swift is already a first-class citizen on Apple platforms, and <a href="https://skip.dev/docs/native/">Skip’s native tooling and technology</a> ensures seamless integration with Android and Compose as well.</p>
<p>Whether you’re creating a single dual-platform app like we did in <a href="https://skip.dev/blog/skip-native-tech-preview/">Part 1</a>, separate iOS and Android apps with a shared model layer and bespoke interfaces like we did in this article, or anything in between, sharing code with Swift can save you significant time and effort when writing your app. More important than the up front savings, though, is the savings over time. A shared Swift codebase will eliminate endless hours of repeated bug fixes, enhancements, team coordination, and general maintenance over the life of your software.</p>androidcross-platformjetpack-composemobilenativeskipswiftswiftuitoolchainDecember Skip Newsletterhttps://skip.dev/blog/newsletter-december-2024/https://skip.dev/blog/newsletter-december-2024/Tue, 17 Dec 2024 00:00:00 GMT<p>
<b>Skip December Newsletter</b>
</p>
<p>Welcome to the December edition of the Skip.tools newsletter! This month we will showcase some of the improvements and advancements we've made to the Skip platform, along with some current events and a peek at our upcoming roadmap.</p>
<p>
<b>Skip and Compiled Swift on Android</b>
</p>
<p>
The big news this month is the release of the Skip's
<b>native</b>
compiled Swift technology preview! Now you are not limited to just using transpiled packages in Skip apps: you can also embed fully native Swift using our Android toolchain and transparent bridge generation. The SkipFuse framework enables you to move seamlessly between native Swift and your transpiled Jetpack Compose user interface. This unlocks the entire universe of pure-Swift packages for use in your Android app! Read the introductory blog post at
<a href="https://skip.dev/blog/skip-native-tech-preview/">/blog/skip-native-tech-preview/</a>
and then browse the full documentation at
<a href="https://skip.dev/docs/native/">/docs/native/</a>.
</p>
<p>
<img alt="Screenshot of native toochain development" src="https://assets.skip.dev/screens/skip-native-splash.png">
</p>
<p>
<b>New SkipUI Features</b>
</p>
<p>SkipUI is the framework that turns your SwiftUI into Jetpack Compose for Android. It enables you to write a single user-interface for both platforms using their platform-native toolkits. SkipUI supports converting nearly all SwiftUI constructs into Compose, but there are sometimes minor deficiencies and quirks that need to be implemented separately for Android. Over the past weeks, we've improved SkipUI with:</p>
<ul>
<li>Support for custom SVG images in asset catalogs</li>
<li>Enabling .alert() sheets to containTextField and SecureField views</li>
<li>Support for .rotation3DEffect for all views</li>
<li>Implementing .interactiveDismissDisabled to conditionally prevent interactive dismissal of sheets</li>
</ul>
<p>
You can always get the latest Skip features and fixes from right within Xcode, by simply clicking
<code>File > Packages > Update to Latest Versions</code>. And if you are building from the command-line,
<code>swift package update</code>
will do the same thing.
</p>
<p>
<b>Tip: Customizing with Android-only SwiftUI modifiers</b>
</p>
<p>
SkipUI supports Android-specific SwiftUI modifiers to customize Material colors, components, and effects. Check out the "Material" section of our SkipUI documentation to see how:
<a href="https://skip.dev/docs/modules/skip-ui/#material">/docs/modules/skip-ui/#material</a>
</p>
<p>
<b>Skip on Talking Kotlin</b>
</p>
<p>
We were thrilled to join hosts Sebastian and
<meta charset="UTF-8">
Márton on the JetBrains
<em>Talking Kotlin</em>
podcast! The episode was just released, and you can listen to it at
<a href="https://talkingkotlin.com/going-from-swift-to-kotlin-with-skip/">https://talkingkotlin.com/going-from-swift-to-kotlin-with-skip/</a>
or watch it at
<a href="https://www.youtube.com/watch?v=mig81rSWVqM">https://www.youtube.com/watch?v=mig81rSWVqM</a>. “Going from Swift to Kotlin with Skip: In a slightly unconventional episode, Sebastian and Márton talk to the founders of Skip, an iOS-to-Android, Swift-to-Kotlin transpiler solution. Marc and Abe have a background working on both Apple platforms and the JVM, and their latest project is a bridge across these two ecosystems.”
</p>
<p>
<b>Android Police Interview</b>
</p>
<p>
Another instance of Skip in the press was an interview with the popular
<i>Android Police</i>
publication, titled: “How the development wall between Android and iOS may soon come down”. You can read the whole thing at:
<a href="https://www.androidpolice.com/skip-interview">https://www.androidpolice.com/skip-interview</a>
</p>
<p>
<b>That's all for now</b>
</p>
<p>
You can follow us on Mastodon at
<a href="https://mas.to/@skiptools">https://mas.to/@skiptools</a>, and join in the Skip discussions at
<a href="http://forums.skip.dev/">http://forums.skip.dev/</a>. The Skip FAQ at
<a href="https://skip.dev/docs/faq/">/docs/faq/</a>
is there to answer any questions, and be sure to check out the video tours at
<a href="https://skip.dev/tour/">/tour/</a>. And, as always, you can reach out directly to us on our Slack channel at
<a href="https://skip.dev/slack/">/slack/</a>.
</p>
<p>Happy Skipping!</p>newsletterannouncementsnative-swiftskipfuseswiftandroidswiftuiskipuiproduct-updatesNative Swift on Android, Part 2: Your First Swift Android Apphttps://skip.dev/blog/skip-native-tech-preview/https://skip.dev/blog/skip-native-tech-preview/Tue, 03 Dec 2024 00:00:00 GMT<p>Native Swift on Android, Part 2: Your First Swift Android App</p>
<div><h2 id="introduction">Introduction</h2></div>
<p>Swift is Apple’s recommended language for app development, and with good reason. Its safety, efficiency, and expressiveness have made it easier than ever to build fast, polished, and robust apps for the Apple ecosystem. Recent stories about <a href="https://www.swift.org/blog/swift-everywhere-windows-interop/" rel="nofollow" target="_blank">Swift on Windows<span> ↗</span></a> and <a href="https://www.swift.org/blog/byte-sized-swift-tiny-games-playdate/" rel="nofollow" target="_blank">Swift on the Playdate<span> ↗</span></a> highlight developers’ desire to take advantage of Swift on other platforms too. In this series, we explore writing native Swift apps for Android with <a href="https://skip.dev/">Skip</a>.</p>
<p>Since its <a href="https://skip.dev/blog/skip-1_0-release/">1.0 release</a> earlier this year, Skip has allowed developers to create cross-platform iOS and Android apps in Swift and SwiftUI by <a href="https://en.wikipedia.org/wiki/Source-to-source_compiler" rel="nofollow" target="_blank"><em>transpiling</em><span> ↗</span></a> your Swift to Android’s native Kotlin language. Now, the Skip team is thrilled to give you the ability to use native, <strong>compiled</strong> Swift for cross-platform development as well.</p>
<p><a href="https://skip.dev/blog/native-swift-on-android-1/">Part 1</a> of this series described bringing a native Swift toolchain to Android. Being able to compile Swift on Android, however, is only the first small step towards real-world applications. In this <a href="#next">and other</a> installments, we introduce the other pieces necessary to go from printing “Hello World” on the console to shipping real apps on the Play Store:</p>
<ul>
<li>Integration of Swift functionality like logging and networking with the Android operating system.</li>
<li>Bridging technology for using Android’s Kotlin/Java API from Swift, and for using Swift API from Kotlin/Java.</li>
<li>The ability to power Jetpack Compose and shared SwiftUI user interfaces with native Swift <code dir="auto">@Observables</code>.</li>
<li>Xcode integration and tooling to build and deploy across both iOS and Android.</li>
</ul>
<div><h2 id="the-hello-swift-app">The “Hello Swift” App</h2></div>
<p>The best way to learn is often by example. This post introduces you to native Swift apps on Android by exploring the “Hello Swift” app. This is the starter app that Skip generates when you initialize a new project, and it provides a fully-configured launch point for your own cross-platform Swift app development.</p>
<p>Before we can explore the sample, though, we have to install the tools necessary to create it - including Swift for Android!</p>
<p>First, ensure that you are on a macOS 14+ machine with Xcode 16, <a href="https://developer.android.com/studio" rel="nofollow" target="_blank">Android Studio<span> ↗</span></a>, and <a href="https://brew.sh" rel="nofollow" target="_blank">Homebrew<span> ↗</span></a> installed.</p>
<p>Next, open Terminal and type the following commands to install Skip and the native Android toolchain.</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>brew install skiptools/skip/skip</span></div></div><div><div><span>skip android sdk install</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>Finally, verify that everything is working with an additional Terminal command:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>skip checkup --native</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<aside aria-label="Note"><p aria-hidden="true">Note</p><div><p>For detailed installation instructions, see the <a href="https://skip.dev/docs/gettingstarted/">Getting Started documentation</a>. If any steps in the checkup command fail, consult the generated log file, which should contain an error message describing the failure. You can seek assistance on our <a href="https://skip.dev/slack/">Slack</a> or <a href="https://forums.skip.dev" rel="nofollow" target="_blank">discussion forums<span> ↗</span></a>.</p></div></aside>
<p>If everything passes successfully, you can now create your first cross-platform native Swift app with the command:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>skip init --native-model --open-xcode --appid=com.xyz.HelloSwift hello-swift HelloSwift HelloSwiftModel</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>That’s a long one! This tells Skip to initialize a new native Swift starter app and open it in Xcode. The app will use the <code dir="auto">hello-swift</code> project folder, and it will be divided into two modules: a <code dir="auto">HelloSwift</code> UI layer and a <code dir="auto">HelloSwiftModel</code> model layer.</p>
<p>When you enter this command, Skip will generate the project, perform some checks, and then the app will open in Xcode. Before running it, though, you must first launch the Android emulator by opening <code dir="auto">Android Studio.app</code> and selecting the <code dir="auto">Virtual Device Manager</code> from the ellipsis menu of the Welcome dialog. From there, use the plus toolbar button to <code dir="auto">Create Device</code> (e.g., “Pixel 6”) and then <code dir="auto">Launch</code> the emulator.</p>
<img alt="Screenshot of the Android Studio Device Manager" src="https://assets.skip.dev/intro/device_manager.png">
<p>Finally, select your desired iOS simulator in Xcode, then build and run the “HelloSwift” target.</p>
<p>The first build will take some time to compile the Skip libraries, and you may be prompted with a dialog to affirm that you trust the Skip plugin. Once the build and run action completes, the app will open in the selected iOS simulator, and at the same time the generated Android app will launch in the currently-running Android emulator.</p>
<p><a href="https://assets.skip.dev/screens/skip-native-splash.png" target="_blank"><img alt="Screenshot of the Hello Swift native app" src="https://assets.skip.dev/screens/skip-native-splash.png"></a></p>
<div><h3 id="app-overview">App Overview</h3></div>
<p>“Hello Swift” is a very simple <a href="https://en.wikipedia.org/wiki/Create,_read,_update_and_delete" rel="nofollow" target="_blank">CRUD<span> ↗</span></a> app that contains a list of dated items. You can browse the full source code in Xcode, or online in its <a href="https://github.com/skiptools/skipapp-hiya" rel="nofollow" target="_blank">GitHub repository<span> ↗</span></a>. At a high level, the Xcode project embeds a Swift Package Manager package called “HelloSwift”, which contains two targets:</p>
<ol>
<li>The <code dir="auto">HelloSwift</code> module contains the SwiftUI for the app’s user interface. It will run natively on iOS and be transpiled by Skip’s “SkipStone” build plugin into Kotlin and Jetpack Compose for Android.</li>
<li><code dir="auto">HelloSwiftModel</code> is a pure Swift module that contains an <code dir="auto">@Observable ViewModel</code> class. It will be compiled natively for both iOS and Android using the Swift toolchain for each platform.</li>
</ol>
<aside aria-label="Note"><p aria-hidden="true">Note</p><div><p>Why is the <code dir="auto">HelloSwift</code> SwiftUI layer transpiled to Kotlin, rather than also being compiled natively? Because <a href="https://developer.android.com/compose" rel="nofollow" target="_blank">Jetpack Compose<span> ↗</span></a>, Android’s recommended UI toolkit, is a Kotlin API. Skip currently only supports defining SwiftUI views in <em>transpiled</em> targets.</p></div></aside>
<p>The app allows you to add new items with the plus button, and items can be deleted and re-arranged by swiping and dragging. Tapping an item navigates to a form with editable fields for the various properties: title, date, a favorites toggle, and notes. <code dir="auto">HelloSwiftModel</code> defines an item as:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>public</span><span> </span><span>struct</span><span> Item : </span><span>Identifiable</span><span>, </span><span>Hashable</span><span>, </span><span>Codable </span><span>{</span></div></div><div><div><span> </span><span>public</span><span> </span><span>let</span><span> id: UUID</span></div></div><div><div><span> </span><span>public</span><span> </span><span>var</span><span> date: Date</span></div></div><div><div><span> </span><span>public</span><span> </span><span>var</span><span> favorite: </span><span>Bool</span></div></div><div><div><span> </span><span>public</span><span> </span><span>var</span><span> title: </span><span>String</span></div></div><div><div><span> </span><span>public</span><span> </span><span>var</span><span> notes: </span><span>String</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>These items are held by an <code dir="auto">@Observable ViewModel</code> class:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>@Observable</span><span> </span><span>public</span><span> </span><span>class</span><span> ViewModel {</span></div></div><div><div><span> </span><span>public</span><span> </span><span>var</span><span> items: [Item] </span><span>=</span><span> </span><span>loadItems</span><span>() {</span></div></div><div><div><span> </span><span>didSet</span><span> { </span><span>saveItems</span><span>() }</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>And in the <code dir="auto">HelloSwift</code> SwiftUI layer, the notes are managed by a SwiftUI <code dir="auto">List</code> within a <code dir="auto">NavigationStack</code>:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>public</span><span> </span><span>struct</span><span> ContentView: </span><span>View </span><span>{</span></div></div><div><div><span> </span><span>@State</span><span> </span><span>var</span><span> viewModel </span><span>=</span><span> </span><span>ViewModel</span><span>()</span></div></div><div><div>
</div></div><div><div><span> </span><span>public</span><span> </span><span>var</span><span> body: </span><span>some</span><span> View {</span></div></div><div><div><span> </span><span>NavigationStack</span><span> {</span></div></div><div><div><span> </span><span>List</span><span> {</span></div></div><div><div><span> </span><span>ForEach</span><span>(</span><span><span>viewModel.</span><span>items</span></span><span>) { item </span><span>in</span></div></div><div><div><span> </span><span>NavigationLink</span><span>(</span><span><span>value</span><span>: item</span></span><span>) {</span></div></div><div><div><span> </span><span>Label</span><span> {</span></div></div><div><div><span> </span><span>Text</span><span>(</span><span><span>item.</span><span>itemTitle</span></span><span>)</span></div></div><div><div><span><span> </span></span><span>} </span><span>icon</span><span>: {</span></div></div><div><div><span> </span><span>if</span><span> item.favorite {</span></div></div><div><div><span> </span><span>Image</span><span>(</span><span><span>systemName</span><span>: </span></span><span>"</span><span>star.fill</span><span>"</span><span>)</span></div></div><div><div><span><span> </span></span><span>.</span><span>foregroundStyle</span><span>(</span><span><span>.</span><span>yellow</span></span><span>)</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>.</span><span>onDelete</span><span> { offsets </span><span>in</span></div></div><div><div><span><span> </span></span><span>viewModel.</span><span>items</span><span>.</span><span>remove</span><span>(</span><span><span>atOffsets</span><span>: offsets</span></span><span>)</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>.</span><span>onMove</span><span> { fromOffsets, toOffset </span><span>in</span></div></div><div><div><span><span> </span></span><span>viewModel.</span><span>items</span><span>.</span><span>move</span><span>(</span><span><span>fromOffsets</span><span>: fromOffsets, </span><span>toOffset</span><span>: toOffset</span></span><span>)</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>.</span><span>navigationTitle</span><span>(</span><span>Text</span><span>(</span><span>"</span><span>\(</span><span>viewModel.</span><span>items</span><span>.count</span><span>)</span><span> Items</span><span>"</span><span>))</span></div></div><div><div><span><span> </span></span><span>.</span><span>navigationDestination</span><span>(</span><span><span>for</span><span>: Item.</span></span><span>self</span><span>) { item </span><span>in</span></div></div><div><div><span> </span><span>ItemView</span><span>(</span><span><span>item</span><span>: item, </span><span>viewModel</span><span>: $viewModel</span></span><span>)</span></div></div><div><div><span><span> </span></span><span>.</span><span>navigationTitle</span><span>(</span><span><span>item.</span><span>itemTitle</span></span><span>)</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>.</span><span>toolbar</span><span> {</span></div></div><div><div><span> </span><span>ToolbarItemGroup</span><span> {</span></div></div><div><div><span> </span><span>Button</span><span> {</span></div></div><div><div><span> </span><span>withAnimation</span><span> {</span></div></div><div><div><span><span> </span></span><span>viewModel.</span><span>items</span><span>.</span><span>insert</span><span>(</span><span>Item</span><span>()</span><span><span>, </span><span>at</span><span>: </span></span><span>0</span><span>)</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>} </span><span>label</span><span>: {</span></div></div><div><div><span> </span><span>Label</span><span>(</span><span>"</span><span>Add</span><span>"</span><span><span>, </span><span>systemImage</span><span>: </span></span><span>"</span><span>plus</span><span>"</span><span>)</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<div><h3 id="module-interaction">Module Interaction</h3></div>
<p>On iOS, both the <code dir="auto">HelloSwift</code> and <code dir="auto">HelloSwiftModel</code> targets are native Swift, so communication between the two layers is seamlessly handled by the Swift dependency. On Android, however, recall that the <code dir="auto">HelloSwift</code> module’s SwiftUI is transpiled to Kotlin. That Kotlin needs to be able to interact with the <code dir="auto">HelloSwiftModel</code> module’s compiled Swift, which involves bridging the two languages and runtimes.</p>
<p>Skip’s bridging solution is called “SkipFuse”. Using the SkipStone build plugin, SkipFuse automatically generates bridging code that enables transparent communication between the two layers. This is modeled in the following diagram, which illustrates how the two modules are combined into final iOS and Android app packages:</p>
<div>
<p><img src="https://assets.skip.dev/diagrams/skip-diagrams-native-model.svg" alt="Skip Native Diagram"></p>
</div>
<p>The details of Skip’s bridging are discussed in the <a href="https://skip.dev/docs/modes/#bridging">documentation</a>. To summarize, the bridging system parses the public types, properties, and functions of your Swift module and exposes them to the transpiled Kotlin layer of your user interface. It supports the <code dir="auto">Observation</code> framework, so you can use <code dir="auto">@Observable</code> classes to manage application state in a way that is tracked by your UI, ensuring that your data and user interface are always in sync.</p>
<p>The following screenshot shows a split view between the <code dir="auto">HelloSwift</code> module’s <code dir="auto">ContentView.swift</code> and the <code dir="auto">HelloSwiftModel</code> module’s <code dir="auto">ViewModel.swift</code>. It demonstrates how the user interface layer communicates with the model layer in exactly the same way on both iOS and Android, although the latter is crossing a language boundary:</p>
<p><a href="https://assets.skip.dev/screens/skip-native-development.png" target="_blank"><img alt="Screenshot of the Hello Swift native app" src="https://assets.skip.dev/screens/skip-native-development.png"></a></p>
<aside aria-label="Note"><p aria-hidden="true">Note</p><div><p>While this sample uses transpiled SwiftUI for its Android user interface, SkipFuse <code dir="auto">@Observables</code> can power pure Jetpack Compose interfaces as well. We explore sharing a native Swift model between separate iOS SwiftUI and Android Compose apps in <a href="#next">another post</a>.</p></div></aside>
<div><h2 id="toolchain-mechanics">Toolchain Mechanics</h2></div>
<p>Skip integrates with the Xcode and Swift Package Manager build systems using the SkipStone Xcode plugin. This plugin transpiles your non-native modules from Swift to Kotlin, and it generates the needed bridging code for communication between your native Swift modules and Kotlin or Java.</p>
<p>The <code dir="auto">skip init</code> command you used to create the “Hello Swift” app also adds a build script to the generated Xcode project. This build script launches Android’s <a href="https://gradle.org" rel="nofollow" target="_blank">Gradle<span> ↗</span></a> build tool to compile and package the plugin’s output into an Android APK. When your project has native modules, this includes compiling the native code using the Android toolchain described in <a href="https://skip.dev/blog/native-swift-on-android-1/">Part 1</a>.</p>
<p>But how does Skip know which modules to transpile and which are native? Every Skip module must include a <code dir="auto">Skip/skip.yml</code> configuration file in its source folder. Here is the configuration for a native Swift module whose public API is bridged to Kotlin:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>skip</span><span>:</span></div></div><div><div><span> </span><span>mode</span><span>: </span><span>'</span><span>native</span><span>'</span></div></div><div><div><span> </span><span>bridging</span><span>: </span><span>true</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>Once you have specified that a module is bridging, the entire process is automatic. You can iterate on both your native and transpiled code and re-launch the app, and the bridging code will be updated without needing to run an external tool or perform any other manual process.</p>
<div><h2 id="next-steps">Next Steps</h2></div>
<p>Now that you’ve created your first native Swift Android app, what’s next? Well, remember that this is just a starter app designed to get you up and running. It is meant to be torn apart and modified, so feel free to experiment by changing the model and UI in Xcode!</p>
<p>If you’d like to learn much more about SkipFuse, bridging, and native Swift on Android, consider reading our <a href="https://skip.dev/docs/native/">Native Swift Tech Preview</a> documentation. Many of the topics it covers are the subjects of <a href="#next">additional posts</a> in this series. For example, while we saw “Hello Swift” bridge its native Swift model layer to its transpiled Kotlin user interface, we haven’t discussed bridging Kotlin API for use by native Swift at all. The documentation covers this in depth.</p>
<p>You may also be interested in the nascent <a href="https://github.com/swiftlang/swift-java" rel="nofollow" target="_blank">swift-java<span> ↗</span></a> project, which is designed to facilitate communication between server-side Swift and Java libraries. While that is a very different environment than Android apps interacting with modern Kotlin APIs, they do overlap, and you might find <code dir="auto">swift-java's</code> bridging approach useful. We anticipate that as it matures, this bridge and Skip’s native bridging will begin to align more closely in their techniques and implementation details.</p>
<div><h2 id="next">Native Swift on Android Series</h2></div>
<p>Additional posts in the native Swift on Android series:</p>
<ul>
<li><a href="https://skip.dev/blog/native-swift-on-android-1/">Part 1: A native Swift toolchain for Android</a></li>
<li>Part 2: Your first native Swift Android app</li>
<li><a href="https://skip.dev/blog/shared-swift-model/">Part 3: Using a shared native Swift model to power separate SwiftUI iOS and Jetpack Compose Android apps</a></li>
<li>Coming soon: Bridging Kotlin and Java API for consumption by native Swift</li>
<li>Coming soon: Incorporating native Swift, C, and C++ dependencies into your cross-platform Swift apps</li>
</ul>
<div><h2 id="conclusion">Conclusion</h2></div>
<p>Swift’s safety, efficiency, and expressiveness make it an excellent development language, and its use across platforms is spreading. This series focuses on sharing native Swift code between iOS and Android with <a href="https://skip.dev/">Skip</a>. <a href="https://skip.dev/blog/native-swift-on-android-1/">Part 1</a> introduced the native Swift toolchain for Android. Now in Part 2, you’ve created your first cross-platform Swift app. There is a lot of interesting territory that we haven’t yet explored, so check out <a href="https://skip.dev/blog/shared-swift-model/">Part 3</a> and beyond!</p>swiftandroidtutorialskip-fusecross-platformnative-swift-android-seriesSeptember 2024 Mastodon Postshttps://skip.dev/blog/mastodon-posts-2024-09/https://skip.dev/blog/mastodon-posts-2024-09/Mon, 30 Sep 2024 00:00:00 GMT<p><strong>Instructions</strong><br>
Desktop: Hover or click to play videos and GIFs<br>
Mobile: Tap missing videos and GIFs to preview. Tap post to view and play media</p>
<iframe src="https://mas.to/@skiptools/113228071036628010/embed" width="400" height="900" allowfullscreen></iframe>
<iframe src="https://mas.to/@skiptools/113204550483220072/embed" width="400" height="500" allowfullscreen></iframe>
<iframe src="https://mas.to/@skiptools/113204553098117686/embed" width="400" height="500" allowfullscreen></iframe>
<iframe src="https://mas.to/@skiptools/113204554822613516/embed" width="400" height="500" allowfullscreen></iframe>
<iframe src="https://mas.to/@skiptools/113193574983392947/embed" width="400" height="700" allowfullscreen></iframe>
<iframe src="https://mas.to/@skiptools/113165924366763260/embed" width="400" height="400" allowfullscreen></iframe>
<iframe src="https://mas.to/@skiptools/113165470064716258/embed" width="400" height="700" allowfullscreen></iframe>
<iframe src="https://mas.to/@skiptools/113148122228081451/embed" width="400" height="700" allowfullscreen></iframe>
<iframe src="https://mas.to/@skiptools/113148123886901596/embed" width="400" height="400" allowfullscreen></iframe>
<iframe src="https://mas.to/@skiptools/113148125272206578/embed" width="400" height="400" allowfullscreen></iframe>
<iframe src="https://mas.to/@skiptools/113132471560288782/embed" width="400" height="700" allowfullscreen></iframe>
<iframe src="https://mas.to/@skiptools/113120245593312899/embed" width="400" height="900" allowfullscreen></iframe>
<iframe src="https://mas.to/@skiptools/113113988349568150/embed" width="400" height="700" allowfullscreen></iframe>
<iframe src="https://mas.to/@skiptools/113091612900999315/embed" width="400" height="700" allowfullscreen></iframe>
<iframe src="https://mas.to/@skiptools/113085862281177403/embed" width="400" height="500" allowfullscreen></iframe>
<iframe src="https://mas.to/@skiptools/113069611730768381/embed" width="400" height="700" allowfullscreen></iframe>newsmastodonNative Swift on Android, Part 1: Setup, Compiling, Running, and Testinghttps://skip.dev/blog/native-swift-on-android-1/https://skip.dev/blog/native-swift-on-android-1/Wed, 11 Sep 2024 00:00:00 GMT<p>You may already be familiar with <a href="https://skip.dev/">Skip</a> as a tool for bringing your Swift iOS apps to Android. Skip takes a novel <em>transpilation</em> approach, where we integrate with the Xcode build system to convert your Swift code into <a href="https://kotlinlang.org" rel="nofollow" target="_blank">Kotlin<span> ↗</span></a>. This allows us to create an Android library for every build of your Swift package, or to launch an Android version of your SwiftUI app on every Xcode <code dir="auto">Run</code>.</p>
<p>We’ve <a href="https://skip.dev/blog/bringing-swift-to-android/">discussed the advantages</a> of a transpilation-based strategy in the past. But despite the fact that Android is a Java/Kotlin-oriented platform, there are also significant benefits to compiled code. Skip has featured support for integrating with <a href="https://skip.dev/blog/sharing-c-between-swift-and-kotlin/">C code</a> on both Android and iOS for a long time. It only makes sense that our <em>transpiled</em> Swift code should also integrate with <em>compiled</em> Swift code.</p>
<p><img src="https://assets.skip.dev/images/Swift-Android.svg" alt="Swift Android Logo">
{: style=“text-align: center; width: 200px; margin: auto;”}</p>
<p>And so we are excited to announce the first technology preview of a <strong>native Swift toolchain and driver for Android</strong>! This toolset enables developers to build and run Swift executables and test cases on a connected Android device or emulator.</p>
<div><h2 id="getting-started">Getting Started</h2></div>
<p>On a macOS development machine with
<a href="https://developer.apple.com/xcode/" rel="nofollow" target="_blank">Xcode<span> ↗</span></a> and
<a href="https://brew.sh" rel="nofollow" target="_blank">Homebrew<span> ↗</span></a> installed,
you can install the Swift 6.0 Android toolchain by opening a terminal and running:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>brew install skiptools/skip/[email protected]</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>This will download the Swift Android SDK, along with all the dependencies it
needs to build, run, and test Swift packages on Android.</p>
<p>If you’re an existing Skip user, make sure to also update your <code dir="auto">skip</code> copy to version 1.1.1+:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>skip upgrade</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<div><h3 id="android-emulator-setup">Android Emulator Setup</h3></div>
<p>Unless you have an Android device handy,
you will need to install the Android emulator in order to run executables and test cases
in a simulated Android environment. The simplest way to do this is to download
and install <a href="https://developer.android.com/studio" rel="nofollow" target="_blank">Android Studio<span> ↗</span></a>, then
launch it and open the “Virtual Device Manager” from the “More Actions” (or ellipsis menu) of
the “Welcome to Android Studio” dialog. On the resulting “Device Manager” screen, select “Create virtual device”.</p>
<div>
<a href="https://assets.skip.dev/android-emulator-setup/emulator_1_setup_welcome_screen.png" target="_blank"><img src="https://assets.skip.dev/android-emulator-setup/emulator_1_setup_welcome_screen.png" alt="Android Emulator Setup 1: Welcome Screen"></a>
<a href="https://assets.skip.dev/android-emulator-setup/emulator_2_setup_device_manager.png" target="_blank"><img src="https://assets.skip.dev/android-emulator-setup/emulator_2_setup_device_manager.png" alt="Android Emulator Setup 2: Device Manager"></a>
</div>
<p>On the “Select Hardware” screen, select a device (e.g., “Pixel 6”) and then on the “Select a system image” screen select one of the recommended images (e.g., “UpsideDownCake”, a.k.a. API 34), and then on the next screen name the device and select “Finish”. When you return to the “Device Manager” screen, you will see a new device (like “Pixel 6 API 34”), which you can then launch with the triangular play button. A little window titled “Android Emulator” will appear and the operating system will boot.</p>
<div>
<a href="https://assets.skip.dev/android-emulator-setup/emulator_3_avd_select_hardware.png" target="_blank"><img src="https://assets.skip.dev/android-emulator-setup/emulator_3_avd_select_hardware.png" alt="Android Emulator Setup 3: Select Hardware"></a>
<a href="https://assets.skip.dev/android-emulator-setup/emulator_4_avd_system_image.png" target="_blank"><img src="https://assets.skip.dev/android-emulator-setup/emulator_4_avd_system_image.png" alt="Android Emulator Setup 4: Select System Image"></a>
<a href="https://assets.skip.dev/android-emulator-setup/emulator_5_avd_verify.png" target="_blank"><img src="https://assets.skip.dev/android-emulator-setup/emulator_5_avd_verify.png" alt="Android Emulator Setup 5: Verify Connfiguration"></a>
</div>
<div>
<a href="https://assets.skip.dev/android-emulator-setup/emulator_6_setup_device_manager.png" target="_blank"><img src="https://assets.skip.dev/android-emulator-setup/emulator_6_setup_device_manager.png" alt="Android Emulator Setup 6: Device Manager"></a>
<a href="https://assets.skip.dev/android-emulator-setup/emulator_7_running.png" target="_blank"><img src="https://assets.skip.dev/android-emulator-setup/emulator_7_running.png" alt="Android Emulator Setup 6: Running Emulator"></a>
</div>
<aside aria-label="Caution"><p aria-hidden="true">Caution</p><div><p>To verify that the emulator is working, open a terminal and run the command <code dir="auto">adb devices</code>. It should output <code dir="auto">List of devices attached</code> followed by a single line with the device identifier, such as <code dir="auto">emulator-5554</code>. If you encounter issues, or for more information on setting up the Android emulator, see the <a href="https://developer.android.com/studio/run/managing-avds" rel="nofollow" target="_blank">Create and manage virtual devices<span> ↗</span></a> documentation.</p></div></aside>
<!--
Can install and configure an "Android Virtual Device" (AVD) with the following commands.
For an ARM ("M-Series") macOS host:
```
sdkmanager "build-tools;35.0.0" "platform-tools" "emulator" "system-images;android-35;google_apis;arm64-v8a" "platforms;android-35"
avdmanager create avd -n "Pixel_7" -d "pixel_7" -k "system-images;android-35;google_apis;arm64-v8a"
```
These commands will be somewhat different if you are running on an
Intel machine. For these, you would replace "arm64-v8a" with "x86_64", as follows:
```
sdkmanager "build-tools;35.0.0" "platform-tools" "emulator" "system-images;android-35;google_apis;x86_64" "platforms;android-35"
avdmanager create avd -n "Pixel_7" -d "pixel_7" -k "system-images;android-35;google_apis;x86_64"
```
Once you have created the AVD on your ARM or Intel machine, you can launch the emulator with the commands:
```
avdmanager list avd
~/Library/Android/sdk/emulator/emulator -avd Pixel_7
```
-->
<div><h2 id="running-swift-hello-world-on-android">Running Swift “Hello World” on Android</h2></div>
<p>Now that you have everything set up and have launched an Android emulator (or connected a physical Android device with <a href="https://developer.android.com/studio/debug/dev-options" rel="nofollow" target="_blank">developer mode enabled<span> ↗</span></a>), it’s time to run some Swift!</p>
<p>Open a terminal and create a new Swift command-line executable called “HelloSwift”:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>% mkdir HelloSwift</span></div></div><div><div><span>% cd HelloSwift</span></div></div><div><div><span>% swift package init --type=executable</span></div></div><div><div>
</div></div><div><div><span>Creating executable package: HelloSwift</span></div></div><div><div><span>Creating Package.swift</span></div></div><div><div><span>Creating Sources/main.swift</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>Just to make sure it works on macOS, run the program with the standard <code dir="auto">swift run</code> command:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>% swift run HelloSwift</span></div></div><div><div>
</div></div><div><div><span>Building for debugging...</span></div></div><div><div><span>Build of product 'HelloSwift' complete! (1.80s)</span></div></div><div><div>
</div></div><div><div><span>Hello, world!</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>And now, we will build and run it on the Android emulator (or device) using the Swift Android driver, which we include as part of the <code dir="auto">skip</code> tool that was installed along with the toolchain:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>% skip android run HelloSwift</span></div></div><div><div>
</div></div><div><div><span>Building for debugging...</span></div></div><div><div><span>Build complete! (10.90s)</span></div></div><div><div>
</div></div><div><div><span>[✓] Check Swift Package (0.68s)</span></div></div><div><div><span>[✓] Connecting to Android (0.05s)</span></div></div><div><div><span>[✓] Copying executable files (0.25s)</span></div></div><div><div>
</div></div><div><div><span>Hello, world!</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<aside aria-label="Danger"><p aria-hidden="true">Danger</p><div><p>If you have installed a version of Xcode that uses a different Swift version than the toolchain (e.g. Xcode 16, which includes Swift 6.0), you may encounter errors when building for Android. In these cases, you can work around this by downloading an earlier version of Xcode from <a href="https://developer.apple.com/download/applications/" rel="nofollow" target="_blank">developer.apple.com<span> ↗</span></a> (e.g., Xcode 15.4) and then referencing the install location with the DEVELOPER_DIR environment variable.</p></div></aside>
<p>Viola! There’s Swift running on Android. And just to prove to that we are really running on a different host, edit the <code dir="auto">Sources/main.swift</code> file with your favorite editor (or run <code dir="auto">xed Sources/main.swift</code> to edit it in Xcode), and add a platform check:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>#</span><span>if</span><span> </span><span>os</span><span>(</span><span>Android</span><span>)</span></div></div><div><div><span>print</span><span>(</span><span>"</span><span>Hello, Android!</span><span>"</span><span>)</span></div></div><div><div><span>#</span><span>elseif</span><span> </span><span>os</span><span>(</span><span>macOS</span><span>)</span></div></div><div><div><span>print</span><span>(</span><span>"</span><span>Hello, macOS!</span><span>"</span><span>)</span></div></div><div><div><span>#</span><span>else</span></div></div><div><div><span>print</span><span>(</span><span>"</span><span>Hello, someone other platform…</span><span>"</span><span>)</span></div></div><div><div><span>#</span><span>endif</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>Then run it on both macOS and Android:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>% swift run HelloSwift</span></div></div><div><div>
</div></div><div><div><span>Building for debugging...</span></div></div><div><div><span>Build of product 'HelloSwift' complete! (0.47s)</span></div></div><div><div>
</div></div><div><div><span>Hello, macOS!</span></div></div><div><div>
</div></div><div><div><span>% skip android run HelloSwift</span></div></div><div><div>
</div></div><div><div><span>Building for debugging...</span></div></div><div><div><span>Build complete! (0.89s)</span></div></div><div><div>
</div></div><div><div><span>[✓] Check Swift Package (0.23s)</span></div></div><div><div><span>[✓] Connecting to Android (0.04s)</span></div></div><div><div><span>[✓] Copying executable files (0.23s)</span></div></div><div><div>
</div></div><div><div><span>Hello, Android!</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<div><h2 id="running-swift-test-cases-on-android">Running Swift Test Cases on Android</h2></div>
<p>Command-line tools are fun, but to really exercise Swift on Android, we
want to be able to run test suites.
This is how developers interested in creating cross-platform frameworks will
be able to check for – and resolve – issues with their Swift code arising from
platform differences.</p>
<p>Fortunately the <code dir="auto">skip android</code> driver includes not just the <code dir="auto">run</code> command,
but also the <code dir="auto">test</code> command, which will connect to the Android emulator/device
and run through an <a href="https://developer.apple.com/documentation/xctest" rel="nofollow" target="_blank">XCTest<span> ↗</span></a> test suite
in the same way as <code dir="auto">swift test</code> does for macOS.</p>
<p>To demonstrate, we can run the test suite for Apple’s
<a href="https://github.com/apple/swift-algorithms.git" rel="nofollow" target="_blank">swift-algorithms<span> ↗</span></a> package,
to make sure it runs correctly on Android:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>% git clone https://github.com/apple/swift-algorithms.git</span></div></div><div><div><span>Cloning into 'swift-algorithms'...</span></div></div><div><div><span>…</span></div></div><div><div><span>Resolving deltas: 100% (1054/1054), done.</span></div></div><div><div>
</div></div><div><div><span>% cd swift-algorithms</span></div></div><div><div><span>% skip android test</span></div></div><div><div>
</div></div><div><div><span>Fetching https://github.com/apple/swift-numerics.git from cache</span></div></div><div><div><span>Fetched https://github.com/apple/swift-numerics.git from cache (0.87s)</span></div></div><div><div><span>Computing version for https://github.com/apple/swift-numerics.git</span></div></div><div><div><span>Computed https://github.com/apple/swift-numerics.git at 1.0.2 (0.57s)</span></div></div><div><div><span>Creating working copy for https://github.com/apple/swift-numerics.git</span></div></div><div><div><span>Working copy of https://github.com/apple/swift-numerics.git resolved at 1.0.2</span></div></div><div><div>
</div></div><div><div><span>Building for debugging...</span></div></div><div><div><span>…</span></div></div><div><div><span>[92/93] Linking swift-algorithmsPackageTests.xctest</span></div></div><div><div><span>Build complete! (25.91s)</span></div></div><div><div>
</div></div><div><div><span>[✓] Check Swift Package (0.74s)</span></div></div><div><div><span>[✓] Connecting to Android (0.06s)</span></div></div><div><div><span>[✓] Copying test files (0.27s)</span></div></div><div><div>
</div></div><div><div><span>Test Suite 'All tests' started at 2024-09-10 20:24:17.770</span></div></div><div><div><span>Test Suite 'swift-algorithms-C7A0585A-0DC2-4937-869A-8FD5E482398C.xctest' started at 2024-09-10 20:24:17.776</span></div></div><div><div><span>Test Suite 'AdjacentPairsTests' started at 2024-09-10 20:24:17.776</span></div></div><div><div><span>Test Case 'AdjacentPairsTests.testEmptySequence' started at 2024-09-10 20:24:17.776</span></div></div><div><div><span>Test Case 'AdjacentPairsTests.testEmptySequence' passed (0.001 seconds)</span></div></div><div><div><span>…</span></div></div><div><div><span>Test Case 'WindowsTests.testWindowsSecondAndLast' started at 2024-09-10 20:24:20.480</span></div></div><div><div><span>Test Case 'WindowsTests.testWindowsSecondAndLast' passed (0.0 seconds)</span></div></div><div><div><span>Test Suite 'WindowsTests' passed at 2024-09-10 20:24:20.480</span></div></div><div><div><span><span> </span></span><span>Executed 8 tests, with 0 failures (0 unexpected) in 0.004 (0.004) seconds</span></div></div><div><div><span>Test Suite 'swift-algorithms-C7A0585A-0DC2-4937-869A-8FD5E482398C.xctest' passed at 2024-09-10 20:24:20.480</span></div></div><div><div><span><span> </span></span><span>Executed 212 tests, with 0 failures (0 unexpected) in 2.702 (2.702) seconds</span></div></div><div><div><span>Test Suite 'All tests' passed at 2024-09-10 20:24:20.480</span></div></div><div><div><span><span> </span></span><span>Executed 212 tests, with 0 failures (0 unexpected) in 2.702 (2.702) seconds</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>Everything passes. Hooray!</p>
<p>Not every package’s tests will pass so easily: Android is based on Linux – unlike the Darwin/BSD heritage of macOS and iOS – so there may be assumptions your code makes for Darwin that don’t hold true on Linux. Running through a comprehensive test suite is the best way to begin isolating, and then addressing, these platform differences.</p>
<aside aria-label="Tip"><p aria-hidden="true">Tip</p><div><p>If you use GitHub for continuous integration testing of your Swift package, you may want to check out Skip’s <a href="https://github.com/skiptools/swift-android-action/" rel="nofollow" target="_blank">Swift Android Action<span> ↗</span></a> on the GitHub Marketplace, which enables automated Android testing for your Swift package as part of your CI workflow.</p></div></aside>
<aside aria-label="Caution"><p aria-hidden="true">Caution</p><div><p>Note that the Android toolchain does not yet support the new <a href="https://developer.apple.com/documentation/testing/" rel="nofollow" target="_blank">Swift Testing<span> ↗</span></a> framework. Only test that use the <a href="https://developer.apple.com/documentation/xctest" rel="nofollow" target="_blank">XCTest<span> ↗</span></a> framework will currently build and run.</p></div></aside>
<div><h2 id="next-steps-creating-an-app">Next Steps: Creating an App</h2></div>
<p>Command line executables and unit tests are all well and good,
but “Hello World” is not an app. To create an actual Android
app, with access to device capabilities and a graphical user interface, you need to
work with the Android SDK, which is written in Java and Kotlin.
And you need to package and distribute the app in Android’s own
idiomatic way, with self-contained libraries embedded in the application’s assembly.</p>
<p>This is where integration with Skip’s broader ecosystem comes into play. <a href="#next">Additional installments</a> of this series explore Skip’s system for transparently bridging compiled Swift to Java, Kotlin, and transpiled Swift - including Skip’s existing <a href="https://skip.dev/docs/modules/skip-ui">SwiftUI support</a> for Android. This allows the best of all worlds: transpiled Swift to talk to Android libraries, SwiftUI on top of <a href="https://developer.android.com/compose" rel="nofollow" target="_blank">Jetpack Compose<span> ↗</span></a>, and business logic and algorithms implemented in compiled Swift!</p>
<img src="https://assets.skip.dev/images/skip-marketing-preview.jpg" alt="Screenshot">
<aside aria-label="Note"><p aria-hidden="true">Note</p><div><p>We would love to hear feedback from developers on their experience with the tools, and to discuss the best way to get your packages ready for a Swift multi-platform world. Reach out to us on our <a href="https://skip.dev/slack">Slack channel</a> or <a href="https://forums.skip.dev" rel="nofollow" target="_blank">community forums<span> ↗</span></a>.</p></div></aside>
<div><h2 id="next">Native Swift on Android Series</h2></div>
<p>Additional posts in the native Swift on Android series:</p>
<ul>
<li>Part 1: A native Swift toolchain for Android</li>
<li><a href="https://skip.dev/blog/skip-native-tech-preview/">Part 2: Your first native Swift Android app</a></li>
<li><a href="https://skip.dev/blog/shared-swift-model/">Part 3: Using a shared native Swift model to power separate SwiftUI iOS and Jetpack Compose Android apps</a></li>
<li>Coming soon: Bridging Kotlin and Java API for consumption by native Swift</li>
<li>Coming soon: Incorporating native Swift, C, and C++ dependencies into your cross-platform Swift apps</li>
</ul>
<div><h2 id="afterword">Afterword</h2></div>
<!--
Swift is designed to be a portable, multi-platform language.
As well as supporting Apple platforms (macOS, iOS, tvOS, etc.),
Swift can also be used to build programs for
[Linux](https://www.swift.org/blog/swift-linux-port/),
[Windows](https://www.swift.org/blog/swift-on-windows/),
and various embedded devices, such as the
[Raspberry Pi](https://www.swift.org/blog/embedded-swift-examples/)
and the [Playdate](https://www.swift.org/blog/byte-sized-swift-tiny-games-playdate/) game console.
-->
<p>The Swift toolchain for Android is the culmination of many years of community effort, in which we (the Skip team) have played only a very small part.</p>
<p>Even before Swift was made open-source, people have been tinkering with getting
it running on Android, starting with Romain Goyet’s
<a href="https://romain.goyet.com/articles/running_swift_code_on_android/" rel="nofollow" target="_blank">“Running Swift code on Android”<span> ↗</span></a>
attempts in 2015, which got some basic Swift compiling and running on an Android device.
A more practical example came with Geordie J’s
<a href="https://medium.com/@ephemer/how-we-put-an-app-in-the-android-play-store-using-swift-67bd99573e3c" rel="nofollow" target="_blank">“How we put an app in the Android Play Store using Swift”<span> ↗</span></a>
in 2016, where Swift was used in an actual shipping Android app.
Then in 2018, Readdle published
<a href="https://readdle.com/blog/swift-for-android-our-experience-and-tools" rel="nofollow" target="_blank">“Swift for Android: Our Experience and Tools”<span> ↗</span></a>
on integrating Swift into their <em>Spark</em> app for Android.
These articles provide valuable technical insight into the mechanics and complexities involved with cross-compiling Swift for a new platform.</p>
<p>In more recent years, the Swift community has had various collaborative and independent endeavors to develop a usable Swift-on-Android toolchain. Some of the most prominent contributors on GitHub are
<a href="https://github.com/finagolfin" rel="nofollow" target="_blank">@finagolfin<span> ↗</span></a>,
<a href="https://github.com/vgorloff" rel="nofollow" target="_blank">@vgorloff<span> ↗</span></a>,
<a href="https://github.com/andriydruk" rel="nofollow" target="_blank">@andriydruk<span> ↗</span></a>,
<a href="https://github.com/compnerd" rel="nofollow" target="_blank">@compnerd<span> ↗</span></a>,
and <a href="https://github.com/hyp" rel="nofollow" target="_blank">@hyp<span> ↗</span></a>.
Our work merely builds atop of their tireless efforts,
and we expect to continue collaborating with them in the
hopes that Android eventually becomes a <a href="https://www.swift.org/platform-support/" rel="nofollow" target="_blank">fully-supported
platform<span> ↗</span></a> for the Swift language.</p>
<p>Looking towards the future, we are eager for the final release of Swift 6.0, which will enable us to publish a toolchain that supports all the great new concurrency features, as well as the <a href="https://www.swift.org/blog/foundation-preview-now-available/" rel="nofollow" target="_blank">Swift Foundation<span> ↗</span></a> reimplementation of the Foundation C/Objective-C libraries, which will give us the the ability to provide better integration between Foundation idioms (bundles, resources, user defaults, notifications, logging, etc.) and the standard Android patterns. A toolchain is only the first step in making native Swift a viable tool for building high-quality Android apps, but it is an essential component that we are very excited to be adding to the Skip ecosystem.</p>swiftandroidtoolchainnativecross-platformxctestmobile-developmentsdktestingcommand-lineAugust Skip Newsletterhttps://skip.dev/blog/newsletter-august-2024/https://skip.dev/blog/newsletter-august-2024/Fri, 30 Aug 2024 00:00:00 GMT<p>Welcome to the August edition of the Skip.tools newsletter! This month we will showcase some of the improvements and advancements we've made to the Skip platform, along with some current events and a peek at our upcoming roadmap.</p>
<p><b>Skip 1.0</b></p>
<p>The big news this month was the launch of Skip 1.0! After over a year in development, Skip is finally ready for general production use. The launch has made a big splash, even being featured on the front page of <a href="https://news.ycombinator.com/item?id=41384144">Hacker News</a>. There has never been a better time to start a new project with Skip and bring your app to the entire mobile marketplace!</p>
<p><b>New FREE Indie Pricing Tier</b></p>
<p>As part of the general availability of Skip, we are also delighted to announce the new Skip Indie tier, which enables solo developers to use Skip to build their dual-platform projects for free.</p>
<p><b>Markdown Support</b></p>
<p>On a technical front, a long-requested feature for SkipUI has been to support SwiftUI's automatic markdown support for Text elements. Well, now it's here! Styling text with simple markdown elements has never been simpler.</p>
<p><b>Reminder: Skip Showcase on the Stores</b></p>
<p>The Skip Showcase app (<a href="https://skip.dev/docs/samples/skipapp-showcase/">/docs/samples/skipapp-showcase/</a>) has long been our go-to for providing a side-by-side comparison of SwiftUI components with the Jetpack Compose equivalents that SkipUI provides. Browsing thought these components simultaneously on an iPhone and Android device gives a good sense Skip's capabilities and power, and is a great way to demonstrate Skip's benefits to project managers and stakeholders before breaking ground on a new project.</p>
<p>In order to make it even easier to get this handy app on your devices, we've published the Skip Showcase app to both the Apple App Store (<a href="https://apps.apple.com/us/app/skip-showcase/id6474885022">https://apps.apple.com/us/app/skip-showcase/id6474885022</a>) as well as the Google Play Store (<a href="https://play.google.com/store/apps/details?id=org.appfair.app.Showcase">https://play.google.com/store/apps/details?id=org.appfair.app.Showcase</a>). This enables you to quickly grab a demo app that highlights Skip's power, and feel for yourself the benefit of using a genuinely native app on both platforms. Download it today and see for yourself what Skip can do!</p>
<p><b>New Skip Showreel Video</b></p>
<p>We've published a new 3-minute video summarizing Skip's capabilities. This is a great video to share with your colleagues and management to highlight some of the benefits of using Skip to bring your app to the entire matketplace. Check it out at <a href="https://skip.dev/tour/skip-showreel/">/tour/skip-showreel/</a></p>
<p><a href="https://skip.dev/tour/skip-showreel/"><img alt="Video poster image" src="https://assets.skip.dev/tour/showreel_video_poster.png"></a></p>
<p><b>That's all for now!</b></p>
<p>
You can follow us on Mastodon at
<a href="https://mas.to/@skiptools">https://mas.to/@skiptools</a>, and join in the Skip discussions at
<a href="http://forums.skip.dev/">http://forums.skip.dev/</a>. The Skip FAQ at
<a href="https://skip.dev/docs/faq/">/docs/faq/</a>
is there to answer any questions, and be sure to check out the video tours at
<a href="https://skip.dev/tour/">/tour/</a>. And, as always, you can reach out directly to us on our Slack channel at
<a href="https://skip.dev/slack/">/slack/</a>.
</p>
<p>Happy Skipping!</p>newsletterannouncementsproduct-launchskip-1.0pricingswiftswiftuiandroidcross-platformSkip 1.0 Releasehttps://skip.dev/blog/skip-1_0-release/https://skip.dev/blog/skip-1_0-release/Wed, 14 Aug 2024 00:00:00 GMT<img src="https://assets.skip.dev/images/skip-marketing-preview.jpg" alt="Screenshot">
<p>We’re thrilled to announce the release of Skip 1.0!</p>
<p>Skip brings Swift app development to Android. Share Swift business logic, or write entire cross-platform apps in SwiftUI.</p>
<p>Skip is the only tool that enables you to develop genuinely native apps for <strong>both</strong> major mobile platforms with a single codebase. Under the hood, it uses the vendor-recommended technologies on each OS: Swift and SwiftUI on iOS, Kotlin and Compose on Android. So your apps don’t just “look native”, they <em>are</em> native, with no additional resource-hogging runtime and no uncanny-valley UI replicas.</p>
<p>Skip also gives you complete access to platform libraries. Directly call any Swift or Objective C API on iOS, and any Kotlin or Java API on Android - no complex bridging required!</p>
<p>Skip has been in development for over a year. It has an enthusiastic community of users developing a wide range of apps and continually improving Skip’s ecosystem of cross-platform open source libraries.</p>skipskip-1.0release-announcementcross-platformmobile-developmentswiftswiftuikotlinjetpack-composeandroidiosmobile-frameworksJuly Skip Newsletterhttps://skip.dev/blog/newsletter-july-2024/https://skip.dev/blog/newsletter-july-2024/Wed, 31 Jul 2024 00:00:00 GMT<p>Welcome to the July edition of the Skip.tools newsletter! This month we will showcase some of the improvements and advancements we've made to the Skip platform, along with some current events and a peek at our upcoming roadmap.</p>
<p>
<b>Swift 6 and Kotlin 2 Support</b>
</p>
<p>The past couple of months saw two important major releases that affect anyone writing modern iOS and Android apps. Kotlin 2 was released at the end of May, and a preview of Swift 6 was added to the Xcode 16 beta in June. Both of these language releases are evolutionary, but they did include some important changes and enhancements.</p>
<p>
Skip has kept pace: we now generate Kotlin 2 Android projects by default, and you can use Swift 6 language features like
<a href="https://www.hackingwithswift.com/swift/6.0/typed-throws">typed throws</a>. Some minor Android build file tweaks may be necessary to modernize pre-existing Skip projects, but overall we are delighted how smooth the transition has been. Skip is designed to enable your apps to keep up with the constant evolution of the primary development languages for both iOS and Android.
</p>
<p>
<b>From Scrumdinger to Scrumskipper</b>
</p>
<p>
Honed and updated over the years, Apple’s
<a href="https://developer.apple.com/tutorials/app-dev-training/getting-started-with-scrumdinger">Scrumdinger tutorial</a>
is an hours-long step-by-step guide to building a complete, modern SwiftUI app. It exercises both built-in UI components and custom drawing, and it takes advantage of Swift language features like Codable for persistence. As its rather unique name implies, the Scrumdinger app allows users to create and manage agile programming scrums on their phones.
</p>
<p>
In our
<a href="https://skip.dev/blog/scrumskipper/">blog post</a>, we show how we took the Scrumdinger app and brought it to Android through the power of Skip. This new "Scrumskipper" app demonstrates how an existing iOS-only app can be incrementally turned into a dual-platform iOS+Android app.
</p>
<p>
<b>Refreshable lists, GeometryReader, and ScrollViewReader</b>
</p>
<p>The pull-to-refresh gesture has been a standard affordance in mobile apps for updating list contents for some time now, and SwiftUI has had built-in support for the operation since last year. We've brought this great feature over to Android by bridging SwiftUI’s .refreshable() modifier to an experimental Compose API for supporting the pull-to-refresh operation, enabling you to add in support for list refreshability with one line of code.</p>
<p>In addition, we've added some more advanced SwiftUI API support, including the ability to exactly identify locations in SwiftUI views using GeometryReader and the ability to jump to individual list elements using ScrollViewReader.</p>
<p>
<b>User Contributions: SkipAV and SkipFirebase</b>
</p>
<p>
All the Skip runtime frameworks are free and open-source software, from the low-level
<a href="https://skip.dev/docs/modules/skip-foundation/">SkipFoundation</a>
to the high level
<a href="https://skip.dev/docs/modules/skip-ui/">SkipUI</a>. In addition, we have a whole constellation of optional frameworks that enable additional functionality, from SQLite database support (<a href="https://skip.dev/docs/modules/skip-sql/">SkipSQL</a>) to Lottie animations (<a href="https://skip.dev/docs/modules/skip-motion/">SkipMotion</a>).
</p>
<p>
One of our frameworks –
<a href="https://github.com/skiptools/skip-av">SkipAV</a>
– enables bridging a subset of the AVKit framework for audio and video support. The initial release included only very basic support for playing videos, but recently a user who was interested in the project added support for recording from the microphone, along with some audio playback improvements.
</p>
<p>
Another of our frameworks,
<a href="https://github.com/skiptools/skip-firebase">SkipFirebase</a>, provides support for Google Firebase, a very popular backend-as-a-service platform used in many mobile applications. And while our original release mostly just supported Firestore – the database layer of Firebase – another interested user recently contributed support for the Auth component, which greatly improves the utility of the framework for all Skip users.
</p>
<p>
These are just two examples of recent community contributions to the Skip ecosystem. If you would like to learn more about how to help improve Skip's support for various Android features, check out our
<a href="https://skip.dev/docs/contributing/">contribution guide</a>.
</p>
<p>
<b>That's all for now</b>
</p>
<p>
You can follow us on Mastodon at
<a href="https://mas.to/@skiptools">https://mas.to/@skiptools</a>, and join in the Skip discussions at
<a href="http://forums.skip.dev/">http://forums.skip.dev/</a>. The Skip FAQ at
<a href="https://skip.dev/docs/faq/">/docs/faq/</a>
is there to answer any questions, and be sure to check out the video tours at
<a href="https://skip.dev/tour/">/tour/</a>. And, as always, you can reach out directly to us on our Slack channel at
<a href="https://skip.dev/slack/">/slack/</a>.
</p>
<p>Happy Skipping!</p>newsletterannouncementsswift-6kotlin-2swiftuiskipuiproduct-updatescommunityopen-sourceScrumskipper: Running Apple's SwiftUI sample app on Androidhttps://skip.dev/blog/scrumskipper/https://skip.dev/blog/scrumskipper/Fri, 12 Jul 2024 00:00:00 GMT<p><a href="https://developer.apple.com/tutorials/app-dev-training/getting-started-with-scrumdinger" rel="nofollow" target="_blank">Scrumdinger<span> ↗</span></a> is Apple’s canonical SwiftUI iPhone app. In this post, we’ll use <a href="https://skip.dev" rel="nofollow" target="_blank">Skip<span> ↗</span></a> to run Scrumdinger as a native Android app as well!</p>
<aside aria-label="Note"><p aria-hidden="true">Note</p><div><p>You can also watch a <a href="https://skip.dev/tour/scrumskipper/">tour video</a> of this process.</p></div></aside>
<img alt="Recording of Scrumdinger app operating on iOS and Android simultaneously" src="https://assets.skip.dev/images/scrumskipper/launch.gif">
<div><h2 id="overview">Overview</h2></div>
<p>Honed and updated over the years, Apple’s <a href="https://developer.apple.com/tutorials/app-dev-training/getting-started-with-scrumdinger" rel="nofollow" target="_blank">Scrumdinger tutorial<span> ↗</span></a> is an hours-long step-by-step guide to building a complete, modern SwiftUI app. It exercises both built-in UI components and custom drawing, and it takes advantage of Swift language features like <code dir="auto">Codable</code> for persistence. As its rather unique name implies, the Scrumdinger app allows users to create and manage <a href="https://www.atlassian.com/agile/scrum" rel="nofollow" target="_blank">agile programming scrums<span> ↗</span></a> on their phones.</p>
<p>This blog post begins where Apple’s tutorial ends. We’ll start with the final Scrumdinger source code and walk you through the process of bringing the full app to Android using Skip. You’ll learn the general steps involved in bringing an existing app to Android, and you’ll become familiar with the types of issues you may encounter and how to overcome them. Let’s get started!</p>
<div><h2 id="scrumdinger-for-iphone">Scrumdinger for iPhone</h2></div>
<p>You can get the full Scrumdinger source code from the <a href="https://developer.apple.com/tutorials/app-dev-training/transcribing-speech-to-text" rel="nofollow" target="_blank">last page<span> ↗</span></a> of Apple’s SwiftUI tutorial. Here’s a direct link to the zipped Xcode project:</p>
<p><a href="https://docs-assets.developer.apple.com/published/9d1c4a1d2dcd046ee8e30ad15f20f6f3/TranscribingSpeechToText.zip" rel="nofollow" target="_blank">https://docs-assets.developer.apple.com/published/9d1c4a1d2dcd046ee8e30ad15f20f6f3/TranscribingSpeechToText.zip<span> ↗</span></a></p>
<p>Download and expand the zip file. Assuming you have the latest <a href="https://developer.apple.com/xcode/" rel="nofollow" target="_blank">Xcode<span> ↗</span></a> installed, you can run the iPhone app by opening <code dir="auto">TranscribingSpeechToText/Complete/Scrumdinger.xcodeproj</code>. The first time you attempt to open it, you may need to confirm that you trust the download. Once Xcode has loaded the project, select the iOS Simulator you’d like to use and hit the Run button!</p>
<img alt="Screenshot of the iOS app's scrum list" src="https://assets.skip.dev/images/scrumskipper/ios-final-scrums.png">
<img alt="Screenshot of the iOS app's scrum detail" src="https://assets.skip.dev/images/scrumskipper/ios-final-detail.png">
<img alt="Screenshot of the iOS app's meeting view" src="https://assets.skip.dev/images/scrumskipper/ios-final-meeting.png">
<p>Play around with the app - this is what we’re going to bring to Android. First, however, we need to install Skip.</p>
<div><h2 id="skip">Skip</h2></div>
<p><a href="https://skip.dev" rel="nofollow" target="_blank">Skip<span> ↗</span></a> is a tool for building fully native iOS and Android apps from a single Swift and SwiftUI codebase. It works by transpiling your Swift into Android’s <a href="https://kotlinlang.org" rel="nofollow" target="_blank">Kotlin<span> ↗</span></a> development language and adapting your SwiftUI to Android’s native <a href="https://developer.android.com/develop/ui/compose" rel="nofollow" target="_blank">Jetpack Compose<span> ↗</span></a> UI framework.</p>
<p>Skip’s Android version of Scrumdinger won’t be pixel-identical to the iOS version, and it shouldn’t be. Rather, we believe in using the native UI framework and controls on each platform. This gives the best possible user experience, avoiding the uncanny-valley feel of non-native solutions.</p>
<p>Follow the <a href="https://skip.dev/docs/gettingstarted/">Getting Started</a> guide to install Skip and your Android environment, including <a href="https://developer.android.com/studio" rel="nofollow" target="_blank">Android Studio<span> ↗</span></a>. Next, launch Android Studio and open the Virtual Device Manager from the ellipsis menu of the Welcome dialog. From there, Create Device (e.g., “Pixel 6”) and then start the Emulator. Skip needs a connected Android device or Emulator to run the Android version of your app.</p>
<img alt="Screenshot of Android Studio Device Manager" src="https://assets.skip.dev/images/scrumskipper/emulator.png">
<p>Now we’re ready to turn Scrumdinger into a dual-platform Skip app.</p>
<div><h2 id="scrumskipper">Scrumskipper</h2></div>
<p>It <a href="https://skip.dev/docs/project-types/#existing_development">isn’t too hard</a> to update an existing Swift Package Manager package to use Skip. Updating an existing <em>app</em>, however, is a different story. Building for Android requires a specific folder structure and <code dir="auto">xcodeproj</code> configuration. We recommend creating a new Skip Xcode project, then importing the old project’s code and resources.</p>
<p>Enter the following command in Terminal to initialize Scrumskipper, your dual-platform version of the Scrumdinger app.</p>
<div><figure><figcaption><span></span></figcaption><pre><code><div><div><span>skip init --open-xcode --appid=com.xyz.Scrumskipper scrum-skipper Scrumskipper</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>This will create a template SwiftUI app and open it in Xcode. Let’s run the template as-is to make sure it’s working: select your desired iOS Simulator in Xcode, and hit the Run button. If you just installed or updated Skip, you may have to trust the Skip plugin:</p>
<img alt="Xcode's Trust Plugin dialog" src="https://assets.skip.dev/images/scrumskipper/trust-plugin.png">
<p>If all goes well, you should see something like the following:</p>
<img alt="Screenshot of template app running in iOS Simulator and Android Emulator" src="https://assets.skip.dev/images/scrumskipper/template-app.png">
<p>Great! Next, copy Scrumdinger’s source code to Scrumskipper:</p>
<ol>
<li>
<p>Drag the <code dir="auto">Scrumdinger/Models</code> and <code dir="auto">Scrumdinger/Views</code> folders from Scrumdinger’s Xcode window into the <code dir="auto">Scrumskipper/Sources/Scrumskipper/</code> folder in Scrumskipper’s window.</p>
<img alt="Copy source folders from Scrumdinger to Scrumskipper" src="https://assets.skip.dev/images/scrumskipper/copy-source.png">
</li>
<li>
<p>Replace Scrumskipper’s <code dir="auto">ContentView</code> body with the content of Scrumdinger’s primary <code dir="auto">WindowGroup</code>. Scrumskipper’s <code dir="auto">ContentView</code> should now look like this:</p>
</li>
</ol>
<div><figure><figcaption></figcaption><pre><code><div><div><span>import</span><span> SwiftUI</span></div></div><div><div>
</div></div><div><div><span>public</span><span> </span><span>struct</span><span> ContentView: </span><span>View </span><span>{</span></div></div><div><div><span> </span><span>@StateObject</span><span> </span><span>private</span><span> </span><span>var</span><span> store </span><span>=</span><span> </span><span>ScrumStore</span><span>()</span></div></div><div><div><span> </span><span>@State</span><span> </span><span>private</span><span> </span><span>var</span><span> errorWrapper: ErrorWrapper</span><span>?</span></div></div><div><div>
</div></div><div><div><span> </span><span>public</span><span> </span><span>init</span><span>()</span><span> {</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div>
</div></div><div><div><span> </span><span>public</span><span> </span><span>var</span><span> body: </span><span>some</span><span> View {</span></div></div><div><div><span> </span><span>ScrumsView</span><span>(</span><span><span>scrums</span><span>: $store.</span><span>scrums</span></span><span>) {</span></div></div><div><div><span> </span><span>Task</span><span> {</span></div></div><div><div><span> </span><span>do</span><span> {</span></div></div><div><div><span> </span><span>try</span><span> </span><span>await</span><span> store.</span><span>save</span><span>(</span><span><span>scrums</span><span>: store.</span><span>scrums</span></span><span>)</span></div></div><div><div><span><span> </span></span><span>} </span><span>catch</span><span> {</span></div></div><div><div><span><span> </span></span><span>errorWrapper </span><span>=</span><span> </span><span>ErrorWrapper</span><span>(</span><span><span>error</span><span>: error,</span></span></div></div><div><div><span><span> </span></span><span>guidance</span><span>: </span><span>"</span><span>Try again later.</span><span>"</span><span>)</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>.</span><span>task</span><span> {</span></div></div><div><div><span> </span><span>do</span><span> {</span></div></div><div><div><span> </span><span>try</span><span> </span><span>await</span><span> store.</span><span>load</span><span>()</span></div></div><div><div><span><span> </span></span><span>} </span><span>catch</span><span> {</span></div></div><div><div><span><span> </span></span><span>errorWrapper </span><span>=</span><span> </span><span>ErrorWrapper</span><span>(</span><span><span>error</span><span>: error,</span></span></div></div><div><div><span><span> </span></span><span>guidance</span><span>: </span><span>"</span><span>Scrumdinger will load sample data and continue.</span><span>"</span><span>)</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>.</span><span>sheet</span><span>(</span><span><span>item</span><span>: $errorWrapper</span></span><span>) {</span></div></div><div><div><span><span> </span></span><span>store.</span><span>scrums</span><span> </span><span>=</span><span> DailyScrum.</span><span>sampleData</span></div></div><div><div><span><span> </span></span><span>} </span><span>content</span><span>: { wrapper </span><span>in</span></div></div><div><div><span> </span><span>ErrorView</span><span>(</span><span><span>errorWrapper</span><span>: wrapper</span></span><span>)</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span>}</span></div></div><div><div>
</div></div><div><div><span>#Preview</span><span> {</span></div></div><div><div><span> </span><span>ContentView</span><span>()</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>That’s it! You’ve now created Scrumskipper, a dual-platform app with all of Scrumdinger’s source code.</p>
<div><h2 id="migration-process">Migration Process</h2></div>
<p>It’s the moment of truth: hit that Xcode Run button!</p>
<p>Almost immediately, you’ll get an API unavailable error like this one:</p>
<img alt="Xcode API unavailable error from Skip" src="https://assets.skip.dev/images/scrumskipper/api-unavailable-error.png">
<p>This is our first hint that <strong>migrating an existing iOS codebase to Android is not trivial</strong>, even with Skip. Starting a <em>new</em> app with Skip can be a lot of fun, because it’s relatively easy to avoid problematic patterns and APIs, and you can tackle any issues one at a time as they appear. But when you take on an existing codebase, you get hit with everything at once. Even if Skip perfectly translates 95+% of your original Swift source and API calls - code that was certainly never intended to be cross-platform - that can leave dozens or even hundreds of errors to deal with!</p>
<p>It’s important to remember, though, that while fixing that remaining 5% can be a <a href="https://www.merriam-webster.com/dictionary/slog" rel="nofollow" target="_blank">slog<span> ↗</span></a>, it is still 20 times less work than a 100% Android rewrite! And once you’ve worked through the errors, you’ll have a wonderfully maintainable, unified Swift and SwiftUI codebase moving forward. So let’s roll up our sleeves and get started, beginning with the error above.</p>
<p>The pictured error message says that the <code dir="auto">Color(_ name:)</code> constructor isn’t available in Skip. Each of Skip’s major <a href="https://skip.dev/docs/modules/">frameworks</a> includes a listing you can consult of the API that is supported on Android. These listings are constantly expanding as we port additional functionality. For example, here is the table of <a href="https://skip.dev/docs/modules/skip-ui/#supported-swiftui">supported SwiftUI</a>.</p>
<p>When an API is unsupported, that does <em>not</em> mean you can’t use it in your app! Skip never forces you to compromise your iOS app. Rather, it means that you have to find a solution for your Android version. That could mean <a href="https://skip.dev/docs/contributing/">contributing</a> an implementation for the missing API, but more often you’ll just want to take a different Android code path. To keep your iOS code intact but create an alternate Android code path, use <code dir="auto">#if SKIP</code> (or <code dir="auto">#if !SKIP</code>) compiler directives. The <a href="https://skip.dev/docs/platformcustomization/">Skip documentation</a> covers compiler directives and other platform customization techniques in detail. Let’s update the problematic code in <code dir="auto">Theme.swift</code> to use named colors on iOS, but fall back to a constant color on Android until we implement a solution:</p>
<p>Change:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>var</span><span> mainColor: Color {</span></div></div><div><div><span> </span><span>Color</span><span>(</span><span>rawValue</span><span>)</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>To:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>var</span><span> mainColor: Color {</span></div></div><div><div><span><span> </span></span><span>#</span><span>if</span><span> !SKIP</span></div></div><div><div><span> </span><span>Color</span><span>(</span><span>rawValue</span><span>)</span></div></div><div><div><span><span> </span></span><span>#</span><span>else</span></div></div><div><div><span> </span><span>// TODO</span></div></div><div><div><span><span> </span></span><span>Color.</span><span>yellow</span></div></div><div><div><span><span> </span></span><span>#</span><span>endif</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>This technique works for SwiftUI modifiers as well. For example, while Skip is able to translate many of SwiftUI’s accessibility modifiers for Android (in fact Skip’s native-UI approach excels in accessibility), it doesn’t have a translation for the <code dir="auto">.accessibilityElement(children:)</code> modifier. So, update from this:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>HStack</span><span> {</span></div></div><div><div><span> </span><span>Label</span><span>(</span><span>"</span><span>Length</span><span>"</span><span><span>, </span><span>systemImage</span><span>: </span></span><span>"</span><span>clock</span><span>"</span><span>)</span></div></div><div><div><span> </span><span>Spacer</span><span>()</span></div></div><div><div><span> </span><span>Text</span><span>(</span><span>"</span><span>\(</span><span>scrum.</span><span>lengthInMinutes</span><span>)</span><span> minutes</span><span>"</span><span>)</span></div></div><div><div><span>}</span></div></div><div><div><span>.</span><span>accessibilityElement</span><span>(</span><span><span>children</span><span>: .</span><span>combine</span></span><span>)</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>To this:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>HStack</span><span> {</span></div></div><div><div><span> </span><span>Label</span><span>(</span><span>"</span><span>Length</span><span>"</span><span><span>, </span><span>systemImage</span><span>: </span></span><span>"</span><span>clock</span><span>"</span><span>)</span></div></div><div><div><span> </span><span>Spacer</span><span>()</span></div></div><div><div><span> </span><span>Text</span><span>(</span><span>"</span><span>\(</span><span>scrum.</span><span>lengthInMinutes</span><span>)</span><span> minutes</span><span>"</span><span>)</span></div></div><div><div><span>}</span></div></div><div><div><span>#</span><span>if</span><span> !SKIP</span></div></div><div><div><span>.</span><span>accessibilityElement</span><span>(</span><span><span>children</span><span>: .</span><span>combine</span></span><span>)</span></div></div><div><div><span>#</span><span>endif</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>The iOS version of the app is unaffected, and the Android build can proceed without the unsupported modifier.</p>
<div><h2 id="rinse-lather-repeat">Rinse, Lather, Repeat</h2></div>
<p>Getting Scrumskipper to successfully build as an iOS <em>and</em> Android app is a repetition of the process we began above:</p>
<ol>
<li>Hit the Run button.</li>
<li>View the next batch of <a href="https://skip.dev/docs/app-development/#build-errors">Xcode errors</a> from the transpiler or the Kotlin compiler.</li>
<li>Use compiler directives to exclude the source causing the error from the Android build.</li>
</ol>
<p>If you see this process through as we did, you’ll end up using <code dir="auto">#if SKIP</code> and <code dir="auto">#if !SKIP</code> approximately 25 times in the ~1,500 lines of Scrumdinger’s Swift and SwiftUI source. In addition to the aforementioned <code dir="auto">Color(_ name:)</code> and <code dir="auto">.accessibilityElement(children:)</code> APIs, here is a full accounting of Scrumdinger code that causes errors:</p>
<ul>
<li>The <code dir="auto">Speech</code> and <code dir="auto">AVFoundation</code> frameworks used in Scrumdinger are not supported. While Skip does have a minimal <code dir="auto">AVFoundation</code> implementation for Android, it is not complete as of this writing. These are examples of cases where you would likely use Skip’s <a href="https://skip.dev/docs/dependencies/">dependency</a> support to integrate Android-specific libraries for the missing functionality.</li>
<li><code dir="auto">Timer.tolerance</code> is not supported.</li>
<li><code dir="auto">.ultraThinMaterial</code> is not supported.</li>
<li><code dir="auto">ListFormatter</code> is not supported. We replaced <code dir="auto">ListFormatter.localizedString(byJoining: attendees.map { $0.name })</code> with <code dir="auto">attendees.map { $0.name }.joined(separator: ", ")</code>.</li>
<li>SwiftUI’s <code dir="auto">ProgressViewStyle</code> is not yet supported.</li>
<li>SwiftUI’s <code dir="auto">LabelStyle</code> is not yet supported.</li>
<li>The <code dir="auto">Theme</code> enum’s <code dir="auto">name</code> property causes an error because all Kotlin enums inherit a built-in, non-overridable <code dir="auto">name</code> property already. We changed the property to <code dir="auto">themeName</code>.</li>
</ul>
<p>You can view the final result in the source of our <code dir="auto">skipapp-scrumskipper</code> <a href="https://skip.dev/docs/samples/skipapp-scrumskipper/">sample app</a>.</p>
<p>After using compiler directives to work around these errors, we have liftoff! Scrumskipper launches on both the iOS Simulator and Android Emulator. Clearly, however, it needs additional work.</p>
<img alt="Screenshot of the Android app's scrum list" src="https://assets.skip.dev/images/scrumskipper/android-initial-scrums.png">
<img alt="Screenshot of the Android app's scrum detail" src="https://assets.skip.dev/images/scrumskipper/android-initial-detail.png">
<img alt="Screenshot of the Android app's meeting view" src="https://assets.skip.dev/images/scrumskipper/android-initial-meeting.png">
<div><h2 id="fixups">Fixups</h2></div>
<p>As you explore the app, you’ll find many things that are missing or broken. Fortunately, it turns out that the problems are all easily fixed. View our <a href="https://skip.dev/docs/samples/skipapp-scrumskipper/">sample app</a> to see the completed code. We give a brief description of each issue below.</p>
<div><h3 id="ios-app-resources">iOS App Resources</h3></div>
<p>Copying over Scrumdinger’s source code wasn’t quite enough. We forgot to copy its <code dir="auto">ding.wav</code> resource and more importantly, the <code dir="auto">Info.plist</code> keys that give the app permission to use the microphone and speech recognition. Without these keys, the iOS app will crash when you attempt to start a meeting.</p>
<div><h3 id="android-sf-symbols">Android SF Symbols</h3></div>
<p>Scrumdinger uses SF Symbols for all of its in-app images. Android obviously doesn’t include these out of the box, but Skip allows you to easily add symbols to your app. Just place vector images with the desired symbol names in your <code dir="auto">Module</code> asset catalog, as described <a href="https://skip.dev/docs/modules/skip-ui/#system-symbols">here</a>. For the purposes of this demo, we exported the symbols from Apple’s SF Symbols app.</p>
<img alt="Exporting a SF Symbol" src="https://assets.skip.dev/images/scrumskipper/symbols-export.png">
<img alt="Copying SF Symbols to Scrumskipper" src="https://assets.skip.dev/images/scrumskipper/symbols-copy.png">
<div><h3 id="meetingview">MeetingView</h3></div>
<p>We made two additional tweaks to <code dir="auto">MeetingView</code>. First, the custom <code dir="auto">MeetingTimerView</code> was not rendering at all on Android. Layout issues like this are rare, but they do occur. Second, we didn’t want the Android navigation bar background to be visible. We added a couple of <code dir="auto">#if SKIP</code> blocks to the view <code dir="auto">body</code> to correct these issues:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>var</span><span> body: </span><span>some</span><span> View {</span></div></div><div><div><span> </span><span>ZStack</span><span> {</span></div></div><div><div><span> </span><span>...</span></div></div><div><div><span> </span><span>VStack</span><span> {</span></div></div><div><div><span> </span><span>MeetingHeaderView</span><span>(</span><span>...</span><span>)</span></div></div><div><div><span> </span><span>MeetingTimerView</span><span>(</span><span>...</span><span>)</span></div></div><div><div><span><span> </span></span><span>#</span><span>if</span><span> SKIP</span></div></div><div><div><span><span> </span></span><span>.</span><span>frame</span><span>(</span><span><span>maxWidth</span><span>: .infinity, </span><span>maxHeight</span><span>: .infinity</span></span><span>)</span></div></div><div><div><span><span> </span></span><span>#</span><span>endif</span></div></div><div><div><span> </span><span>MeetingFooterView</span><span>(</span><span>...</span><span>)</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span> </span><span>...</span></div></div><div><div><span><span> </span></span><span>#</span><span>if</span><span> SKIP</span></div></div><div><div><span><span> </span></span><span>.</span><span>toolbarBackground</span><span>(</span><span><span>.</span><span>hidden</span><span>, </span><span>for</span><span>: .</span><span>navigationBar</span></span><span>)</span></div></div><div><div><span><span> </span></span><span>#</span><span>endif</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<div><h3 id="persistence">Persistence</h3></div>
<p>Scrums were not being saved and restored in the Android version of the app. It turns out that Scrumdinger saved its state on transition to <code dir="auto">ScenePhase.inactive</code>, but Android doesn’t use this phase! Android apps transition directly from <code dir="auto">active</code> to <code dir="auto">background</code> and back. The following simple change fixed the issue:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>struct</span><span> ScrumsView: </span><span>View </span><span>{</span></div></div><div><div><span> </span><span>@Environment</span><span>(\.</span><span>scenePhase</span><span>) </span><span>private</span><span> </span><span>var</span><span> scenePhase</span></div></div><div><div><span> </span><span>...</span></div></div><div><div>
</div></div><div><div><span> </span><span>var</span><span> body: </span><span>some</span><span> View {</span></div></div><div><div><span> </span><span>NavigationStack</span><span> {</span></div></div><div><div><span> </span><span>...</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>.</span><span>onChange</span><span>(</span><span><span>of</span><span>: scenePhase</span></span><span>) { phase </span><span>in</span></div></div><div><div><span><span> </span></span><span>#</span><span>if</span><span> !SKIP</span></div></div><div><div><span> </span><span>if</span><span> phase </span><span>==</span><span> .inactive { </span><span>saveAction</span><span>() }</span></div></div><div><div><span><span> </span></span><span>#</span><span>else</span></div></div><div><div><span> </span><span>if</span><span> phase </span><span>==</span><span> .background { </span><span>saveAction</span><span>() }</span></div></div><div><div><span><span> </span></span><span>#</span><span>endif</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<div><h3 id="named-colors">Named Colors</h3></div>
<p>Remember how we were going to circle back to the <code dir="auto">Theme</code> enum’s named colors on Android? We decided to forgo the asset catalog colors altogether and just define each color programmatically with RGB values.</p>
<div><h3 id="results">Results</h3></div>
<p>The completed native Android app actually looks a lot like its iOS counterpart:</p>
<img alt="Screenshot of the Android app's scrum list" src="https://assets.skip.dev/images/scrumskipper/android-final-scrums.png">
<img alt="Screenshot of the Android app's scrum detail" src="https://assets.skip.dev/images/scrumskipper/android-final-detail.png">
<img alt="Screenshot of the Android app's edit view" src="https://assets.skip.dev/images/scrumskipper/android-final-edit.png">
<img alt="Screenshot of the Android app's meeting view" src="https://assets.skip.dev/images/scrumskipper/android-final-meeting.png">
<div><h2 id="conclusion">Conclusion</h2></div>
<p>Take a moment to reflect on how amazing it is that Apple’s canonical SwiftUI iPhone sample now runs as a fully native Android app. Though Skip excels at <em>new</em> development and the process for bringing Scrumdinger’s existing code to Android wasn’t trivial, it was still an order of magnitude faster than a re-write and did not risk regressions to the iOS version. Moreover, future maintenance and improvements will be extremely efficient thanks to having a single shared Swift and SwiftUI codebase.</p>
<p>The Android version doesn’t yet have all the features of the iOS one, but additional Android functionality can be added over time. Skip <em>never</em> forces you to compromise your iOS app, provides a fast path to a native Android version using your existing code, and has <a href="https://skip.dev/docs/platformcustomization/">excellent integration abilities</a> to enhance and specialize your Android app when the effort warrants it.</p>swiftuijetpack-composeandroidioscross-platformskipswiftkotlinapp-migrationscrumdingerJune Skip Newsletterhttps://skip.dev/blog/newsletter-june-2024/https://skip.dev/blog/newsletter-june-2024/Sun, 30 Jun 2024 00:00:00 GMT<p>Welcome to the June edition of the Skip.tools newsletter! This month we will showcase some of the improvements and advancements we've made to the Skip platform, along with some current events and a peek at our upcoming roadmap.</p>
<p>
<b>New Skip Intro Video</b>
</p>
<p>
We've posted a new Skip "Showreel" video, providing a quick 3-minute overview of Skip and the highlights of using it to build native dual-platform apps. You can find it on YouTube at:
<a href="https://www.youtube.com/watch?v=lQjaaAqgxp4">https://www.youtube.com/watch?v=lQjaaAqgxp4</a>. This and other videos are also available from our Tour page at:
<a href="https://skip.dev/tour/">/tour/</a>. We will be posting new videos in the coming weeks and months, so consider either following us on YouTube, or subscribing to our RSS feed.
</p>
<p>
<img alt="skip-showreel-poster.png" src="https://assets.skip.dev/tour/showreel_video_poster.png">
</p>
<p>
<b>Skip Showcase on the Stores</b>
</p>
<p>
The Skip Showcase app (<a href="https://skip.dev/docs/samples/skipapp-showcase/">/docs/samples/skipapp-showcase/</a>) has long been our go-to for providing a side-by-side comparison of SwiftUI components with the Jetpack Compose equivalents that SkipUI provides. Browsing thought these components simultaneously on an iPhone and Android device gives a good sense Skip's capabilities and power, and is a great way to demonstrate Skip's benefits to project managers and stakeholders before breaking ground on a new project.
</p>
<p>
In order to make it even easier to get this handy app on your devices, we've published the Skip Showcase app to both the
<a href="https://apps.apple.com/us/app/skip-showcase/id6474885022">Apple App Store</a>
as well as the
<a href="https://play.google.com/store/apps/details?id=org.appfair.app.Showcase">Google Play Store</a>. This enables you to quickly grab a demo app that highlights Skip's power, and feel for yourself the benefit of using a genuinely native app on both platforms. Download it today and see for yourself what Skip can do!
</p>
<p>
<b>New Framework: SkipKeychain</b>
</p>
<p>
Using the Keychain has long been the standard way to store bits of sensitive data, such as passwords and notes, on your iOS device. We're happy to announce a brand-new SkipKeychain module that provides an API to read and write sensitive data both on iOS and Android. As with the rest of Skip's library ecosystem, it is free and open-source and available on GitHub at:
<a href="https://github.com/skiptools/skip-keychain/">https://github.com/skiptools/skip-keychain/</a>. We're only on version 0.0.1 right now, but we expect to be able to iterate quickly to add features and functionality that the community wants to see in this nascent project.
</p>
<p>
<b>Skip and Kotlin Multiplatform</b>
</p>
<p>
Skip and Kotlin Multiplatform (KMP) are two sides of the same coin. Skip brings your Swift/iOS codebase to Android, and KMP brings your Kotlin/Android codebase to iOS. Many people have assumed that this diametrical opposition means that the two technologies are incompatible. But this is not the case! KMP modules can be embedded in Skip apps, and they work seamlessly, for the most part, with the Swift-to-Kotlin code transpilation that Skip provides. Check out our deep dive into the integration at
<a href="https://skip.dev/blog/skip-and-kotlin-multiplatform/">/blog/skip-and-kotlin-multiplatform/</a>
and learn how you can take your business-logic KMP modules and integrate them in both the iOS and Android sides of your Skip app.
</p>
<p>
<b>Skip Slack Group</b>
</p>
<p>
By popular demand, we are starting to migrate away from our gitter.im Matrix chat system to a new Skiptools Slack group. Going forward, this will be the preferred medium for live discussions and getting technical help. The Skip team will be standing by to answer questions and help with any issues that members of the community may encounter. You can sign up and join the conversation at:
<a href="https://skip.dev/slack/">/slack/</a>.
</p>
<p>
<b>Skip and Fastlane</b>
</p>
<p>The last mile of app development can be the most grueling. Taking your tested and polished 1.0 app and getting it into the hands of your users ought to be quick and simple, but it isn't. Running the gauntlet of the app store submission process is hard enough when you only target one platform, but when you target both iOS and Android, you need to contend with a plethora of hurdles for both the Apple App Store and the Google Play Store.</p>
<p>
Fortunately, the popular Fastlane tool (
<a href="https://fastlane.tools/">https://fastlane.tools/</a>
) has evolved over the years to help alleviate some of the drudgery of submitting new apps, as well as updated releases, to these storefronts. And we're happy to report that new projects created with the `skip init` command will now include Fastlane templates that provide everything you need to automate your app distributions. Read more about it on our blog post:
<a href="https://skip.dev/blog/skip-and-fastlane/">/blog/skip-and-fastlane/</a>.
</p>
<p>
<b>WWDC, Google I/O, and Skip</b>
</p>
<p>Google I/O 2024 and WWDC 2024, the preeminent conferences for Google and Apple developers alike, went back-to-back in June. These exciting events unveiled a lot of new features to the languages and frameworks that are used daily by mobile app developers. We here on the Skip team are working hard to take advantage of many of the new features that were announced.</p>
<p>Language evolution was announced as well: Kotlin 2.0 (final) was released, and Swift 6.0 (beta) was offered up in preview. As we march towards a Skip 1.0 release, we are going to make sure that all the code we process and generate is compatible with both these next-generation language releases, and takes advantages of as many of the new features as possible, while still remaining compatible with prior source and binary releases. Skip 1.0 is right around the corner, and it will be right up to date with the latest and greatest!</p>
<p>
<b>Thanks!</b>
</p>
<p>
You can follow us on Mastodon at
<a href="https://mas.to/@skiptools">https://mas.to/@skiptools</a>, and join in the Skip discussions at
<a href="http://forums.skip.dev/">http://forums.skip.dev/</a>. The Skip FAQ at
<a href="https://skip.dev/docs/faq/">/docs/faq/</a>
is there to answer any questions, and be sure to check out the video tours at
<a href="https://skip.dev/tour/">/tour/</a>. And, as always, you can reach out directly to us on our Slack channel at
<a href="https://skip.dev/slack/">/slack/</a>.
</p>
<p>Happy Skipping!</p>newsletterannouncementsproduct-updatesskip-modulesfastlanekotlin-multiplatformwwdcgoogle-iocommunitySkip and Kotlin Multiplatformhttps://skip.dev/blog/skip-and-kotlin-multiplatform/https://skip.dev/blog/skip-and-kotlin-multiplatform/Tue, 25 Jun 2024 00:00:00 GMT<ul>
<li>Table of contents
{:toc}</li>
</ul>
<p>Kotlin Multiplatform (KMP) is a technology that enables Kotlin to be compiled natively and used in non-Java environments. Google recommends using KMP for sharing business logic between Android and iOS platforms<sup><a href="#user-content-fn-1" id="user-content-fnref-1" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup>.</p>
<p>In many ways, Skip and KMP are inverses of each other, in that:</p>
<ul>
<li>Skip brings your Swift/iOS codebase to Android.</li>
<li>KMP brings your Kotlin/Android codebase to iOS.</li>
</ul>
<p>The mechanics powering these transformations are different – Skip uses source <a href="https://skip.dev/docs/modes/#lite">transpilation</a> to convert Swift into idiomatic Kotlin, whereas KMP compiles Kotlin into native code that presents an Objective-C interface – but the high-level benefits are the same: you can maintain a single codebase for both your iOS and Android app.</p>
<div><h2 id="skip-or-kmp">Skip or KMP?</h2></div>
<p><img src="https://assets.skip.dev/images/skip-or-kmp.jpg" alt="Skip or KMP">
{: style=“text-align: center; width: 50%; margin: auto;”}</p>
<p>We think that Skip is the <a href="https://skip.dev/docs/#skip-versus-other-cross-platform-mobile-frameworks">right way</a> to tackle the challenge of creating genuinely native dual-platform apps. Skip gives you an uncompromised iOS-first development approach: your code is used as-is on iOS devices, with zero bridging and no added runtime or garbage collector<sup><a href="#user-content-fn-2" id="user-content-fnref-2" data-footnote-ref="" aria-describedby="footnote-label">2</a></sup>. Our <a href="https://skip.dev/docs/modules/skip-ui/">SkipUI</a> adaptor framework – which takes your SwiftUI and converts it into Jetpack Compose – allows you to create genuinely native user interfaces for both platforms. And while the Compose Multiplatform<sup><a href="#user-content-fn-3" id="user-content-fnref-3" data-footnote-ref="" aria-describedby="footnote-label">3</a></sup> project adds cross-platform Compose support to KMP, it eschews native components on iOS by default. It utilizes a Flutter-like strategy instead, painting interface elements onto a Skia canvas. This can result in a sub-par experience for iOS users in terms of aesthetics, performance, accessibility, and feel, not to mention limitations on native component integration (something Skip <a href="https://skip.dev/docs/modules/skip-ui/#composeview">excels at</a>). We believe that a premium, no-compromises user experience requires embracing the platform’s native UI toolkit.</p>
<p>If we put the UI layer aside, however, using KMP for logic and model code <em>does</em> have some great benefits. With KMP, you can target not just Android and iOS, but also the web, desktop, and server-side environments, whereas Skip is focused squarely on mobile app development. You can also write and build KMP code on a variety of platforms: macOS, Windows, and Linux. Finally, some organizations might already be heavily invested in the Kotlin/Java ecosystem.</p>
<div><h2 id="skip-and-kmp">Skip <em>and</em> KMP!</h2></div>
<p><img src="https://assets.skip.dev/images/skip-and-kmp.jpg" alt="Skip and KMP">
{: style=“text-align: center; width: 50%; margin: auto;”}</p>
<p>And so it may be the case that you have business logic in one or more KMP modules that you want to use in a cross-platform Android and iOS app. The trend among organizations that have adopted KMP has been to build separate native apps for each platform – using Jetpack Compose (or Views) on Android and SwiftUI (or UIKit) on iOS – and then import their KMP business logic module into those apps. This is much the same as <a href="https://skip.dev/docs/#skip-versus-writing-two-separate-native-apps">writing two separate apps</a>, but with the benefit that some of the business logic can use a shared codebase.</p>
<p>This happens to be a perfect fit for Skip. With Skip/KMP integration, you can build the UI of your app from a single Swift codebase, and at the same time use the KMP module from both the Swift and (transpiled) Kotlin sides of the app. You get all the benefits of a genuinely native user interface, and can still leverage any existing investment in a shared Kotlin codebase. The remainder of this post will outline the details of integrating and accessing a KMP module from a Skip app project.</p>
<div><h2 id="using-a-kmp-library-in-a-skip-project">Using a KMP library in a Skip project</h2></div>
<p>When viewed from the Android side, a KMP module is simply a traditional Kotlin/Gradle project dependency: you add it to your <code dir="auto">build.gradle.kts</code> and import the Kotlin packages in the same way as you use any other Kotlin package. The KMP module has no knowledge of, or dependency on, any Skip libraries or tools.</p>
<p>The iOS side is a bit more involved: the KMP project must be compiled and exported as a native library<sup><a href="#user-content-fn-4" id="user-content-fnref-4" data-footnote-ref="" aria-describedby="footnote-label">4</a></sup> that can be imported into the iOS project. This is done by compiling the KMP project to native code and then exporting it to an xcframework, which is a multi-platform binary framework bundle that is supported by Xcode and SwiftPM.</p>
<p>The resulting project and dependency layout will look like this:</p>
<div>
<p><img src="https://assets.skip.dev/diagrams/skip-diagrams-kmp.svg" alt="Skip KMP Diagram"></p>
</div>
<div><h2 id="adding-a-kmp-dependency-to-a-skip-framework">Adding a KMP dependency to a Skip Framework</h2></div>
<p>In the <code dir="auto">Package.swift</code> file for the <a href="https://github.com/skiptools/skip-kmp-sample.git" rel="nofollow" target="_blank">skip-kmp-sample<span> ↗</span></a> library, we have the Skip-enabled “SkipKMPSample” target with a dependency on a binary target specifying the location and checksum of the compiled xcframework. This enables us to access the Objective-C compiled interface for the KMP library, which is in the separate <a href="https://github.com/skiptools/kmp-library-sample.git" rel="nofollow" target="_blank">kmp-library-sample<span> ↗</span></a> repository containing the Kotlin and Gradle project that builds the library.</p>
<div><figure><figcaption><span>5.9</span></figcaption><pre><code><div><div><span>import</span><span> PackageDescription</span></div></div><div><div>
</div></div><div><div><span>let</span><span> </span><span>package</span><span> </span><span>=</span><span> </span><span>Package</span><span>(</span></div></div><div><div><span><span> </span></span><span>name</span><span>: </span><span>"</span><span>skip-kmp-sample</span><span>"</span><span>,</span></div></div><div><div><span><span> </span></span><span>defaultLocalization</span><span>: </span><span>"</span><span>en</span><span>"</span><span>,</span></div></div><div><div><span><span> </span></span><span>platforms</span><span>: [.</span><span>iOS</span><span>(</span><span><span>.</span><span>v16</span></span><span>)</span><span><span>, .</span><span>macOS</span></span><span>(</span><span><span>.</span><span>v13</span></span><span>)</span><span><span>, .</span><span>tvOS</span></span><span>(</span><span><span>.</span><span>v16</span></span><span>)</span><span><span>, .</span><span>watchOS</span></span><span>(</span><span><span>.</span><span>v9</span></span><span>)</span><span><span>, .</span><span>macCatalyst</span></span><span>(</span><span><span>.</span><span>v16</span></span><span>)</span><span>],</span></div></div><div><div><span><span> </span></span><span>products</span><span>: [</span></div></div><div><div><span><span> </span></span><span>.</span><span>library</span><span>(</span><span><span>name</span><span>: </span></span><span>"</span><span>SkipKMPSample</span><span>"</span><span><span>, </span><span>targets</span><span>: [</span></span><span>"</span><span>SkipKMPSample</span><span>"</span><span>]</span><span>)</span><span>,</span></div></div><div><div><span><span> </span></span><span>],</span></div></div><div><div><span><span> </span></span><span>dependencies</span><span>: [</span></div></div><div><div><span><span> </span></span><span>.</span><span>package</span><span>(</span><span><span>url</span><span>: </span></span><span>"</span><span>https://github.com/skiptools/skip.git</span><span>"</span><span><span>, </span><span>from</span><span>: </span></span><span>"</span><span>0.8.55</span><span>"</span><span>)</span><span>,</span></div></div><div><div><span><span> </span></span><span>.</span><span>package</span><span>(</span><span><span>url</span><span>: </span></span><span>"</span><span>https://github.com/skiptools/skip-foundation.git</span><span>"</span><span><span>, </span><span>from</span><span>: </span></span><span>"</span><span>0.6.12</span><span>"</span><span>)</span></div></div><div><div><span><span> </span></span><span>],</span></div></div><div><div><span><span> </span></span><span>targets</span><span>: [</span></div></div><div><div><span><span> </span></span><span>.</span><span>target</span><span>(</span><span><span>name</span><span>: </span></span><span>"</span><span>SkipKMPSample</span><span>"</span><span><span>, </span><span>dependencies</span><span>: [</span></span></div></div><div><div><span><span> </span></span><span>.</span><span>product</span><span>(</span><span><span>name</span><span>: </span></span><span>"</span><span>SkipFoundation</span><span>"</span><span><span>, </span><span>package</span><span>: </span></span><span>"</span><span>skip-foundation</span><span>"</span><span>)</span><span>,</span></div></div><div><div><span> </span><span>"</span><span>MultiPlatformLibrary</span><span>"</span></div></div><div><div><span><span> </span></span><span>], </span><span>resources</span><span>: [.</span><span>process</span><span>(</span><span>"</span><span>Resources</span><span>"</span><span>)</span><span><span>], </span><span>plugins</span><span>: [.</span><span>plugin</span></span><span>(</span><span><span>name</span><span>: </span></span><span>"</span><span>skipstone</span><span>"</span><span><span>, </span><span>package</span><span>: </span></span><span>"</span><span>skip</span><span>"</span><span>)</span><span>]</span><span>)</span><span>,</span></div></div><div><div><span><span> </span></span><span>.</span><span>testTarget</span><span>(</span><span><span>name</span><span>: </span></span><span>"</span><span>SkipKMPSampleTests</span><span>"</span><span><span>, </span><span>dependencies</span><span>: [</span></span></div></div><div><div><span> </span><span>"</span><span>SkipKMPSample</span><span>"</span><span>,</span></div></div><div><div><span><span> </span></span><span>.</span><span>product</span><span>(</span><span><span>name</span><span>: </span></span><span>"</span><span>SkipTest</span><span>"</span><span><span>, </span><span>package</span><span>: </span></span><span>"</span><span>skip</span><span>"</span><span>)</span></div></div><div><div><span><span> </span></span><span>], </span><span>resources</span><span>: [.</span><span>process</span><span>(</span><span>"</span><span>Resources</span><span>"</span><span>)</span><span><span>], </span><span>plugins</span><span>: [.</span><span>plugin</span></span><span>(</span><span><span>name</span><span>: </span></span><span>"</span><span>skipstone</span><span>"</span><span><span>, </span><span>package</span><span>: </span></span><span>"</span><span>skip</span><span>"</span><span>)</span><span>]</span><span>)</span><span>,</span></div></div><div><div><span><span> </span></span><span>.</span><span>binaryTarget</span><span>(</span><span><span>name</span><span>: </span></span><span>"</span><span>MultiPlatformLibrary</span><span>"</span><span>,</span></div></div><div><div><span><span> </span></span><span>url</span><span>: </span><span>"</span><span>https://github.com/skiptools/kmp-library-sample/releases/download/1.0.4 /MultiPlatformLibrary.xcframework.zip</span><span>"</span><span>,</span></div></div><div><div><span><span> </span></span><span>checksum</span><span>: </span><span>"</span><span>65e97edcdeadade0f10ef0253d0200bce0009fe11f9826dc11ad6d56b6436369</span><span>"</span><span>)</span></div></div><div><div><span><span> </span></span><span>]</span></div></div><div><div><span>)</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>For the transpiled Kotlin side of the Skip framework, we add a Gradle source dependency<sup><a href="#user-content-fn-5" id="user-content-fnref-5" data-footnote-ref="" aria-describedby="footnote-label">5</a></sup> to that same repository. This is accomplished by using the module’s <a href="https://skip.dev/docs/dependencies/"><code dir="auto">skip.yml</code></a> file to add the dependency on the same tagged version as the published xcframework:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>settings</span><span>:</span></div></div><div><div><span> </span><span>contents</span><span>:</span></div></div><div><div><span><span> </span></span><span>- </span><span>block</span><span>: </span><span>'</span><span>sourceControl</span><span>'</span></div></div><div><div><span> </span><span>contents</span><span>:</span></div></div><div><div><span><span> </span></span><span>- </span><span>block</span><span>: </span><span>'</span><span>gitRepository(java.net.URI.create("https://github.com/skiptools/kmp-library-sample.git"))</span><span>'</span></div></div><div><div><span> </span><span>contents</span><span>:</span></div></div><div><div><span><span> </span></span><span>- </span><span>'</span><span>producesModule("kmp-library-sample:multi-platform-library")</span><span>'</span></div></div><div><div>
</div></div><div><div><span>build</span><span>:</span></div></div><div><div><span> </span><span>contents</span><span>:</span></div></div><div><div><span><span> </span></span><span>- </span><span>block</span><span>: </span><span>'</span><span>dependencies</span><span>'</span></div></div><div><div><span> </span><span>contents</span><span>:</span></div></div><div><div><span><span> </span></span><span>- </span><span>'</span><span>implementation("kmp-library-sample:multi-platform-library:1.0.4")</span><span>'</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>The result is that the Kotlin side of the Skip project will depend on the Kotlin library, and the Swift side of the Skip project will access the natively-compiled xcframework of the KMP library via its exported Objective-C interface.</p>
<div><h2 id="using-kmp-code-from-a-skip-app">Using KMP code from a Skip app</h2></div>
<p>Using KMP code from a Skip app is generally the same as using KMP from any other app. As already mentioned, on the Android side, the KMP module is included directly as Kotlin code, and compiled to JVM bytecode along with the rest of your app. On the iOS side, the code is compiled natively to each of the supported architectures (ARM iOS, ARM/X86 iOS Simulator, and ARM/X86 macOS) and bundled into an xcframework. As part of this packaging the KMP compiler generates an Objective-C interface to the native code. This interface can then be used from your Swift through the automatic Objective-C bridging provided by the Swift language.</p>
<p>A simple example can be illustrated using the following Kotlin class:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>class</span><span> SampleClass(</span><span>var</span><span> stringField: String, </span><span>var</span><span> intField: Int, </span><span>val</span><span> doubleField: Double) {</span></div></div><div><div><span> </span><span>fun</span><span> </span><span>addNumbers</span><span>() : Double {</span></div></div><div><div><span> </span><span>return</span><span> intField </span><span>+</span><span> doubleField</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div>
</div></div><div><div><span> </span><span>suspend</span><span> </span><span>fun</span><span> </span><span>asyncFunction</span><span>(duration: Long) {</span></div></div><div><div><span> </span><span>delay</span><span>(duration)</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div>
</div></div><div><div><span><span> </span></span><span>@Throws(Exception::</span><span>class</span><span>)</span></div></div><div><div><span> </span><span>fun</span><span> </span><span>throwingFunction</span><span>() {</span></div></div><div><div><span> </span><span>throw</span><span> </span><span>Exception</span><span>(</span><span>"This function always throws"</span><span>)</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>The Objective-C header created by the Kotlin/Native compiler for this class will look like this:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>__attribute__</span><span>((objc_subclassing_restricted))</span></div></div><div><div><span>__attribute__</span><span>((</span><span>swift_name</span><span>(</span><span>"</span><span>SampleClass</span><span>"</span><span>)))</span></div></div><div><div><span>@interface</span><span> MPLSampleClass : </span><span>MPLBase</span></div></div><div><div>
</div></div><div><div><span>@property</span><span> (</span><span>readonly</span><span>) </span><span>double</span><span> doubleField </span><span>__attribute__</span><span>((</span><span>swift_name</span><span>(</span><span>"</span><span>doubleField</span><span>"</span><span>)));</span></div></div><div><div><span>@property</span><span> </span><span>int32_t</span><span> intField </span><span>__attribute__</span><span>((</span><span>swift_name</span><span>(</span><span>"</span><span>intField</span><span>"</span><span>)));</span></div></div><div><div><span>@property</span><span> </span><span>NSString</span><span> </span><span>*</span><span>stringField </span><span>__attribute__</span><span>((</span><span>swift_name</span><span>(</span><span>"</span><span>stringField</span><span>"</span><span>)));</span></div></div><div><div>
</div></div><div><div><span>- (</span><span>instancetype</span><span>)</span><span>initWithStringField</span><span>:</span><span>(</span><span>NSString</span><span> </span><span>*</span><span>)</span><span>stringField</span><span> </span><span>intField</span><span>:</span><span>(</span><span>int32_t</span><span>)</span><span>intField</span><span> </span><span>doubleField</span><span>:</span><span>(</span><span>double</span><span>)</span><span>doubleField</span><span> __attribute__((</span><span>swift_name</span><span>(</span><span>"</span><span>init(stringField:intField:doubleField:)</span><span>"</span><span>))) </span><span>__attribute__</span><span>((objc_designated_initializer));</span></div></div><div><div>
</div></div><div><div><span>-</span><span> (</span><span>double</span><span>)addNumbers </span><span>__attribute__</span><span>((</span><span>swift_name</span><span>(</span><span>"</span><span>addNumbers()</span><span>"</span><span>)));</span></div></div><div><div>
</div></div><div><div><span>/**</span></div></div><div><div><span><span> </span></span><span>* @note This method converts instances of CancellationException to errors.</span></div></div><div><div><span><span> </span></span><span>* Other uncaught Kotlin exceptions are fatal.</span></div></div><div><div><span>*/</span></div></div><div><div><span>-</span><span> (</span><span>void</span><span>)asyncFunctionDuration:(</span><span>int64_t</span><span>)duration completionHandler:(</span><span>void</span><span> (</span><span>^</span><span>)(</span><span>NSError</span><span> </span><span>*</span><span> _Nullable))completionHandler </span><span>__attribute__</span><span>((</span><span>swift_name</span><span>(</span><span>"</span><span>asyncFunction(duration:completionHandler:)</span><span>"</span><span>)));</span></div></div><div><div>
</div></div><div><div><span>/**</span></div></div><div><div><span><span> </span></span><span>* @note This method converts instances of Exception to errors.</span></div></div><div><div><span><span> </span></span><span>* Other uncaught Kotlin exceptions are fatal.</span></div></div><div><div><span>*/</span></div></div><div><div><span>-</span><span> (</span><span>BOOL</span><span>)throwingFunctionAndReturnError:(</span><span>NSError</span><span> </span><span>*</span><span> _Nullable </span><span>*</span><span> _Nullable)error </span><span>__attribute__</span><span>((</span><span>swift_name</span><span>(</span><span>"</span><span>throwingFunction()</span><span>"</span><span>)));</span></div></div><div><div><span>@end</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>When viewed as Swift, the Objective-C interface will be represented as:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>public</span><span> </span><span>class</span><span> SampleClass: </span><span>NSObject </span><span>{</span></div></div><div><div><span> </span><span>public</span><span> </span><span>var</span><span> stringField: </span><span>String</span><span> { </span><span>get</span><span> </span><span>set</span><span> }</span></div></div><div><div><span> </span><span>public</span><span> </span><span>var</span><span> intField: </span><span>Int32</span><span> { </span><span>get</span><span> </span><span>set</span><span> }</span></div></div><div><div><span> </span><span>public</span><span> </span><span>var</span><span> doubleField: </span><span>Double</span><span> { </span><span>get</span><span> }</span></div></div><div><div>
</div></div><div><div><span> </span><span>public</span><span> </span><span>init</span><span>(</span><span>stringField</span><span>: </span><span>String</span><span>, </span><span>intField</span><span>: </span><span>Int32</span><span>, </span><span>doubleField</span><span>: </span><span>Double</span><span>)</span></div></div><div><div><span> </span><span>public</span><span> </span><span>func</span><span> </span><span>addNumbers</span><span>()</span><span> </span><span>-></span><span> </span><span>Double</span></div></div><div><div><span> </span><span>public</span><span> </span><span>func</span><span> </span><span>asyncFunction</span><span>(</span><span>duration</span><span>: </span><span>Int64</span><span>)</span><span> </span><span>async</span><span> </span><span>throws</span></div></div><div><div><span> </span><span>public</span><span> </span><span>func</span><span> </span><span>throwingFunction</span><span>()</span><span> </span><span>throws</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>This Swift interface derived from the generated Objective-C is idiomatic, much in the same way that Skip’s transpiled code is <a href="https://skip.dev/docs/swiftsupport/">idiomatic Kotlin</a>. This results in code that can be used from both sides of your dual-platform Swift project using the same interface. For example, this Swift code will work on both sides of a Skip project, where the Swift code instantiates the Objective-C class, and the transpiled Kotlin code instantiates the Java class.</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>func</span><span> </span><span>performAdd</span><span>()</span><span> </span><span>-></span><span> </span><span>Double</span><span> {</span></div></div><div><div><span> </span><span>let</span><span> instance </span><span>=</span><span> </span><span>SampleClass</span><span>(</span><span><span>stringField</span><span>: </span></span><span>"</span><span>XYZ</span><span>"</span><span><span>, </span><span>intField</span><span>: </span><span>Int32</span></span><span>(</span><span>123</span><span>)</span><span><span>, </span><span>doubleField</span><span>: </span></span><span>12.23</span><span>)</span></div></div><div><div><span> </span><span>return</span><span> instance.</span><span>addNumbers</span><span>()</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<div><h3 id="kotlin-type-mappings">Kotlin type mappings</h3></div>
<p>The <a href="https://kotlinlang.org/docs/native-objc-interop.html#mappings" rel="nofollow" target="_blank">type mapping<span> ↗</span></a> section of the Interoperability with Swift/Objective-C documentation goes over the automatic conversion of various Kotlin types into their closest Objective-C equivalents. This, in turn, will affect how the Swift types are represented.</p>
<p>These type mappings will typically be the same as the type mappings used by Skip to represent Swift types in Kotlin (such as a Kotlin <code dir="auto">Short</code> being represented by a Swift <code dir="auto">Int16</code>), but they don’t always line up exactly. In these cases, there may need to be some manual coercion of types inside an <a href="https://skip.dev/docs/platformcustomization/#compiler-directives"><code dir="auto">#if SKIP</code> block</a> to get both the Swift and transpiled Kotlin to behave the same.</p>
<div><h3 id="throwing-functions">Throwing functions</h3></div>
<p>Kotlin doesn’t have functions that declare that they might throw an exception (like Swift and Java), but if you add the <code dir="auto">@Throws</code> annotation to a function, the Kotlin/Native compiler will generate Objective-C that accepts a trailing <code dir="auto">NSError</code> pointer argument, which is, in turn, represented in Swift as a throwing function. For example, the following Kotlin:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>@Throws(IllegalArgumentException::</span><span>class</span><span>)</span></div></div><div><div><span>public</span><span> func </span><span>someThrowingFunction</span><span>() {</span></div></div><div><div><span><span> </span></span><span>…</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>will be represented in Swift as:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>func</span><span> </span><span>someThrowingFunction</span><span>()</span><span> </span><span>throws</span><span> {</span></div></div><div><div><span> </span><span>…</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<div><h3 id="async-functions">Async functions</h3></div>
<p>In the same way that Skip transforms Swift <a href="https://skip.dev/docs/swiftsupport/#concurrency">async functions into Kotlin coroutines</a>, the Kotlin/Native compiler will generate Objective-C with a trailing <code dir="auto">completionHandler</code> parameter that will be represented in Swift as an async throwing function.</p>
<p>For example, the Kotlin function:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>suspend</span><span> </span><span>fun</span><span> </span><span>someAsyncFunction</span><span>(argument: String): String {</span></div></div><div><div><span><span> </span></span><span>…</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>will be represented in Swift as:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>func</span><span> </span><span>someAsyncFunction</span><span>(</span><span>argument</span><span>: </span><span>String</span><span>)</span><span> </span><span>async</span><span> </span><span>throws</span><span> {</span></div></div><div><div><span> </span><span>…</span></div></div><div><div><span>}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<div><h2 id="caveats">Caveats</h2></div>
<p>Considering that Skip was not designed with KMP integration in mind – nor vice-versa – it is remarkable how well they work together out of the box. Classes, functions, async, throwable: all work without any special consideration by the Skip transpiler.</p>
<p>That being said, the integration is not perfect. You may encounter some of the following issues:</p>
<ul>
<li>Primitive Boxing: KMP will box primitives when wrapping in collection types like arrays. For example, a Kotlin function that returns an <code dir="auto">[Int]</code> will be represented in Objective-C as an <code dir="auto">NSArray<MPLInt *></code>, where <code dir="auto">MPLInt</code> is an <code dir="auto">NSNumber</code> type. And while the automatic Objective-C bridging will handle converting the <code dir="auto">NSArray</code> into a Swift <code dir="auto">Array</code>, it will not know enough about <code dir="auto">MPLInt</code> to convert it into a Swift <code dir="auto">Int32</code>, so that sort of conversion will need to be handled manually.</li>
<li>Static Functions: KMP doesn’t map <code dir="auto">object</code> functions to Objective-C <code dir="auto">static</code> functions in the way that Skip assumes, but rather handles a Kotlin <code dir="auto">object</code> as a singleton instance of the type accessible through a <code dir="auto">shared</code> property.</li>
</ul>
<!-- - Single KMP Dependency Limitation: an iOS project can have only a single KMP xcframework dependency[^6]. Attempts to import multiple KMP frameworks will result in `duplicate symbols` errors at build time (for static libraries), or errors like `Class MPLKListAsNSArray is implemented in both` (for dynamic libraries). This limits its utility to a single leaf dependency for an app-specific library, rather than, say, enabling multiple independent KMP modules to be used by one or more apps. -->
<p>More can be read about platform-specific behavior in the Kotlin/Native <a href="https://kotlinlang.org/docs/native-ios-integration.html" rel="nofollow" target="_blank">iOS integration<span> ↗</span></a> docs.</p>
<div><h2 id="next-steps">Next steps</h2></div>
<p>To experiment with your own Skip/KMP integrations, we recommend starting with our pair of example repositories:</p>
<ul>
<li><a href="https://github.com/skiptools/kmp-library-sample.git" rel="nofollow" target="_blank">kmp-library-sample<span> ↗</span></a>: a basic KMP project that presents a source Kotlin dependency, and whose <a href="https://github.com/skiptools/kmp-library-sample/releases" rel="nofollow" target="_blank">releases<span> ↗</span></a> publish a binary xcframework artifact.</li>
<li><a href="https://github.com/skiptools/skip-kmp-sample.git" rel="nofollow" target="_blank">skip-kmp-sample<span> ↗</span></a>: a basic Skip project that depends on the <code dir="auto">kmp-library-sample</code>’s published xcframework on the Swift side, and has a source dependency on the <code dir="auto">kmp-library-sample</code> gradle project on the Kotlin side. The test cases in this project utilize Skip’s <a href="https://skip.dev/docs/modules/skip-unit/#parity-testing">parity testing</a> to ensure that the tests pass on each supported architecture: macOS and Robolectric for local testing, as well as iOS and Android for connected testing.</li>
</ul>
<p>These two projects work together to provide a minimal working example of Skip’s KMP integration, and can be used as the basis for further development.</p>
<div><h2 id="conclusion">Conclusion</h2></div>
<p>Skip presents an iOS-first, full-stack approach to writing apps for iOS and Android. From the low-level logic layers to the high-level user interface, Skip provides a vertically integrated stack of frameworks that enable the creation of best-in-class apps using the native UI toolkits for both of the dominant mobile platforms.</p>
<p>Kotlin Multiplatform has benefits too: KMP modules can be written and tested on multiple platforms, and they can target platforms beyond mobile. For this reason, KMP can be a compelling option for people who want to share their mobile app code with the web, desktop, or server-side applications.</p>
<p>KMP code fits nicely with Skip projects, because its idiomatic native Objective-C representation means that, for the most part, it can be used seamlessly from both the source Swift and transpiled Kotlin sides of a project. Whether you are creating KMP modules because you are invested in the Kotlin/Java ecosystem, or because you are starting to migrate away from an Android-centric app infrastructure, Skip provides the ideal complement to your existing KMP code. You can have the genuinely native user interface for both platforms that Skip provides, while at the same time utilizing the Kotlin code that you may have built up over time. It is truly the best of both worlds!</p>
<section data-footnotes="">
<ol>
<li id="user-content-fn-1">
<p>“We use Kotlin Multiplatform within Google and recommend using KMP for sharing business logic between Android and iOS platforms.” – <a href="https://developer.android.com/kotlin/multiplatform" rel="nofollow" target="_blank">https://developer.android.com/kotlin/multiplatform<span> ↗</span></a>. The mobile-specific form of KMP had been known as KMM, or “Kotlin Multiplatform Mobile”, until they recently <a href="https://blog.jetbrains.com/kotlin/2023/07/update-on-the-name-of-kotlin-multiplatform/" rel="nofollow" target="_blank">deprecated the term<span> ↗</span></a>. <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 1">↩</a></p>
</li>
<li id="user-content-fn-2">
<p>KMP embeds a garbage collector into its compiled iOS framework. Kotlin/Native’s GC algorithm is a stop-the-world mark and concurrent sweep collector that does not separate the heap into generations. <a href="https://kotlinlang.org/docs/native-memory-manager.html#garbage-collector" rel="nofollow" target="_blank">https://kotlinlang.org/docs/native-memory-manager.html#garbage-collector<span> ↗</span></a> <a href="#user-content-fnref-2" data-footnote-backref="" aria-label="Back to reference 2">↩</a></p>
</li>
<li id="user-content-fn-3">
<p>“Develop stunning shared UIs for Android, iOS, desktop, and web” – <a href="https://www.jetbrains.com/lp/compose-multiplatform" rel="nofollow" target="_blank">https://www.jetbrains.com/lp/compose-multiplatform<span> ↗</span></a> <a href="#user-content-fnref-3" data-footnote-backref="" aria-label="Back to reference 3">↩</a></p>
</li>
<li id="user-content-fn-4">
<p>Details about how a KMP project can be used to create an xcframework can be found at <a href="https://kotlinlang.org/docs/apple-framework.html" rel="nofollow" target="_blank">https://kotlinlang.org/docs/apple-framework.html<span> ↗</span></a>, and you can reference our <a href="https://github.com/skiptools/kmp-library-sample.git" rel="nofollow" target="_blank">kmp-library-sample<span> ↗</span></a> project for a concrete example. This is another interesting reversal of the normal way of doing things, where the Swift side of an app typically has source dependencies on other SwiftPM packages and the Kotlin side typically has binary dependencies on <code dir="auto">jar</code>/<code dir="auto">aar</code> artifacts published to a Maven repository. When depending on a KMP project, this is reversed: the Swift side has a binary dependency on the xcframework built from the KMP project, and the Kotlin side has a source dependency on the KMP project’s Kotlin/Gradle project. <a href="#user-content-fnref-4" data-footnote-backref="" aria-label="Back to reference 4">↩</a></p>
</li>
<li id="user-content-fn-5">
<p>Note that we could have alternatively depended on a compiled <code dir="auto">.aar</code> published as a pom to a Maven repository, but for expedience we find that using source dependencies are the easiest way to link directly to another git repository. <a href="#user-content-fnref-5" data-footnote-backref="" aria-label="Back to reference 5">↩</a></p>
</li>
</ol>
</section>skipkotlin-multiplatformcross-platformswiftuijetpack-composeswiftkotlinkmpGoing the last mile with Skip and Fastlanehttps://skip.dev/blog/skip-and-fastlane/https://skip.dev/blog/skip-and-fastlane/Wed, 19 Jun 2024 00:00:00 GMT<p>Getting your finished app into the hands of users can be a laborious process. The individual app stores – the Apple App Store the the Google Play Store, primarily – have their own cumbersome web-based processes for uploading the app binary, adding metadata and screenshots, and providing the necessarily content ratings and regulatory information required by various jurisdictions. And once you have gone through all the tedious manual steps needed to release the initial version of the app, each and every update will also need to follow many of those steps all over again.</p>
<p>Fortunately, this process has become so irksome, to so many developers, that a community tool called “Fastlane” was born. In the <a href="https://docs.fastlane.tools" rel="nofollow" target="_blank">documentation<span> ↗</span></a>, it describes itself as:</p>
<blockquote>
<p>fastlane is the easiest way to automate beta deployments and releases for your iOS and Android apps. 🚀 It handles all tedious tasks, like generating screenshots, dealing with code signing, and releasing your application.</p>
</blockquote>
<p>Fastlane is architected as a collection of plugins that handle all manner of app distribution tasks, like packaging, signing, and uploading. It is configured with a platform-specific hierarchy of local text and ruby configuration files, one for <a href="https://docs.fastlane.tools/getting-started/android/setup/" rel="nofollow" target="_blank">Android<span> ↗</span></a> and another for <a href="https://docs.fastlane.tools/getting-started/ios/setup/" rel="nofollow" target="_blank">iOS<span> ↗</span></a>.</p>
<p>Skip 0.8.50 now has built-in support for creating a default fastlane configuration for each of your iOS and Android projects. When you create a new project with the command:</p>
<div><figure><figcaption></figcaption><pre><code><div><div><span>skip init --fastlane --appid=app.bundle.id package-name AppName</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div>
<p>The <code dir="auto">Darwin/</code> and <code dir="auto">Android/</code> folders will each contain a template for the fastlane project, which holds the metadata files that can be edited to fill in information like the app’s title, description, content ratings, and screenshots.</p>
<p>Once you fill in the generated text files with your app’s specific details, such as the title, description, and keywords, you can then use the <code dir="auto">fastlane release</code> command in each of the folders to create new releases of your app and submit them either to the store’s beta test service, or to be reviewed for worldwide release.</p>
<p>Being able to quickly build and upload a new release with a single command is a great help in maintaining a rapid release cadence. It enables “continuous delivery”, defined by <a href="https://en.wikipedia.org/wiki/Continuous_delivery" rel="nofollow" target="_blank">Wikipedia<span> ↗</span></a> as:</p>
<blockquote>
<p>Continuous delivery (CD) is a software engineering approach in which teams produce software in short cycles, ensuring that the software can be reliably released at any time. It aims at building, testing, and releasing software with greater speed and frequency. The approach helps reduce the cost, time, and risk of delivering changes by allowing for more incremental updates to applications in production. A straightforward and repeatable deployment process is important for continuous delivery.</p>
</blockquote>
<p>We are using this process ourselves with those Skip apps that we are delivering to the app stores, like the <a href="https://skip.dev/docs/samples/skipapp-showcase/">Skip Showcase</a> app that demonstrates the various SkipUI components (<a href="https://apps.apple.com/us/app/skip-showcase/id6474885022" rel="nofollow" target="_blank">App Store link<span> ↗</span></a>, <a href="https://play.google.com/store/apps/details?id=org.appfair.app.Showcase" rel="nofollow" target="_blank">Play Store link<span> ↗</span></a>).</p>
<p>Being able to submit a new release to both the major app storefronts with a single command is a joy. It can be used to submit quick fixes, or as part of a continuous integration workflow triggered by tagging your source release. However you use it, Fastlane eliminates much of the repetition and tedium of release management.</p>
<p>For more information on Skip’s Fastlane integration, see the <a href="https://skip.dev/docs/deployment/#fastlane">deployment documentation</a>.</p>skipfastlanedeploymentiosandroidautomationapp-store-submission