<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Playtomic</title>
    <description>The latest articles on Forem by Playtomic (@playtomic).</description>
    <link>https://forem.com/playtomic</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F201%2Feee12a98-255d-45d4-a517-0bf58a08a192.png</url>
      <title>Forem: Playtomic</title>
      <link>https://forem.com/playtomic</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/playtomic"/>
    <language>en</language>
    <item>
      <title>AI &amp; Automation 2025 Yearly Wrap-Up</title>
      <dc:creator>Angel G. Olloqui</dc:creator>
      <pubDate>Fri, 23 Jan 2026 11:06:50 +0000</pubDate>
      <link>https://forem.com/playtomic/ai-automation-2025-yearly-wrap-up-4ckl</link>
      <guid>https://forem.com/playtomic/ai-automation-2025-yearly-wrap-up-4ckl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This post is an adaptation of an internal wrap-up I shared at Playtomic to celebrate our AI journey in 2025. I've kept the public-facing industry insights and general learnings while removing internal-only details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Happy 2026! 🎉 As we kick off the new year, let's take a moment to celebrate 2025—the year AI went from "cool experiment" to "wait, this is actually can change everything." This wrap-up captures the industry's most transformative developments alongside a look at how AI and automation are becoming integral to modern product development and operations. 🎊&lt;/p&gt;




&lt;h2&gt;
  
  
  🌍 AI IN THE INDUSTRY
&lt;/h2&gt;

&lt;p&gt;💡 &lt;em&gt;What happened in AI this year? &lt;a href="https://docs.google.com/presentation/d/1xiLl0VdrlNMAei8pmaX4ojIOfej6lhvZbOIK7Z6C-Go/edit?slide=id.g309a25a756d_0_85#slide=id.g309a25a756d_0_85" rel="noopener noreferrer"&gt;A lot.&lt;/a&gt; Here's what you need to know:&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🧠 The Tech Race Heats Up
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;OpenAI still leads, but the competition is fierce.&lt;/strong&gt; China's open-source models (DeepSeek, Qwen, Kimi and GLM) are gaining ground fast, while Google's Gemini 3 Pro &lt;a href="https://llm-stats.com/" rel="noopener noreferrer"&gt;dominates reasoning benchmarks&lt;/a&gt; and Claude remains the go-to for software development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reasoning models are evolving rapidly.&lt;/strong&gt; These "think before you answer" AIs can now plan, reflect, self-correct, and work over longer time horizons—making them smarter problem-solvers. But humans still outperform AI on &lt;a href="https://galileo.ai/blog/humanitys-last-exam-ai-benchmark" rel="noopener noreferrer"&gt;Humanity's Last Exam&lt;/a&gt;, a benchmark designed to test the very edge of human knowledge. For now, we're still the champions. 💪&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multimodal, Image and Video AI just leveled up.&lt;/strong&gt; &lt;a href="https://aistudio.google.com/models/veo-3" rel="noopener noreferrer"&gt;Google's Veo 3&lt;/a&gt; and &lt;a href="https://openai.com/index/sora-2/" rel="noopener noreferrer"&gt;OpenAI's Sora 2&lt;/a&gt; now generate high-quality 8-second videos with audio and sound effects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First World models on stage.&lt;/strong&gt; Google also dropped &lt;a href="https://deepmind.google/blog/genie-3-a-new-frontier-for-world-models/" rel="noopener noreferrer"&gt;Genie 3&lt;/a&gt; and Runway &lt;a href="https://runwayml.com/research/introducing-runway-gwm-1?utm_source=www.therundown.ai&amp;amp;utm_medium=newsletter&amp;amp;utm_campaign=disney-s-billion-dollar-ai-bet-on-openai&amp;amp;_bhlid=7146b55d482c5b838bccfed809a074c48378e477#worlds-section" rel="noopener noreferrer"&gt;GWM-1&lt;/a&gt;, interactive world models that creates virtual environments you can explore and interact with in real time. Expect lots of innovation around this on 2026 🎬&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP (Model Context Protocol), aka "USB-C for AI," is officially everywhere&lt;/strong&gt;—companies are racing to support it, even if &lt;a href="https://newsletter.pragmaticengineer.com/p/mcp-deepdive" rel="noopener noreferrer"&gt;users are still figuring out where to plug it in&lt;/a&gt;. 🔌 Think of it as the universal connector AI has been waiting for.&lt;/p&gt;

&lt;h3&gt;
  
  
  💰 Industry &amp;amp; Infrastructure
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Mega investments are pouring in.&lt;/strong&gt; Case in point: &lt;em&gt;&lt;a href="https://en.wikipedia.org/wiki/Stargate_LLC" rel="noopener noreferrer"&gt;Stargate&lt;/a&gt;&lt;/em&gt;—a $500B, 10GW US mega-cluster backed by Altman, SoftBank, Ellison, and Trump. 4 million GPUs working together! 🤯&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chips are still the bottleneck.&lt;/strong&gt; Nvidia produces ~90% of AI's horsepower while US controls 75% of global AI capacity—though China is racing to close the gap despite chip export restrictions. 🔬&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Electricity is now the new limiting factor.&lt;/strong&gt; China added 400GW of power infrastructure in 2024 vs 40GW in the US. Meanwhile, some companies are exploring space-based solar power plants— the sky isn't the limit anymore. 🛰️&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From courtroom to conference room.&lt;/strong&gt; After a wave of lawsuits from Disney, BBC, Getty Images, The New York Times,… over unauthorized training data, 2025 brought détente: licensing deals. Now, content partnerships are the new normal—and a &lt;a href="https://thewaltdisneycompany.com/disney-openai-sora-agreement/?utm_source=www.therundown.ai&amp;amp;utm_medium=newsletter&amp;amp;utm_campaign=disney-s-billion-dollar-ai-bet-on-openai&amp;amp;_bhlid=10d122b4c29835ea12a299cc8644453cb4c1dcc3" rel="noopener noreferrer"&gt;fresh revenue&lt;/a&gt; stream for publishers. 🤝&lt;/p&gt;

&lt;h3&gt;
  
  
  🏢 Enterprise Adoption
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;2025 was the year of AI agents&lt;/strong&gt;—mostly text-based, but voice is ramping up. Still, most companies are stuck in &lt;a href="https://www.mckinsey.com/capabilities/quantumblack/our-insights/the-state-of-ai" rel="noopener noreferrer"&gt;pilot phase&lt;/a&gt;. Meanwhile at Playtomic? We said "hold my racket" 🎾 and shipped agents straight to production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Executives are optimistic, but expertise lags behind.&lt;/strong&gt; &lt;a href="https://www.accenture.com/us-en/insights/consulting/gen-ai-reinventing-enterprise-models" rel="noopener noreferrer"&gt;97% of executives&lt;/a&gt; believe AI will transform their industry and company, yet only 35% say they have the expertise needed to drive those transformations. It seems everyone wants to ride the AI wave, but most are still looking for their surfboard. 🏄‍♂️&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Supernova AI startups"&lt;/strong&gt; are a new category: AI companies growing faster than anything we've seen before. 🚀&lt;/p&gt;

&lt;h3&gt;
  
  
  🌐 Politics &amp;amp; Jobs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;AI politics hardened.&lt;/strong&gt; The US doubled down on "America-first AI," Europe's AI Act stumbled, and China expanded its open-source ecosystem and domestic chip ambitions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Defense goes all-in on AI.&lt;/strong&gt; The US Army dropped a &lt;a href="https://www.palantir.com/offerings/defense/army/" rel="noopener noreferrer"&gt;$10B deal with Palantir&lt;/a&gt;, NATO adopted &lt;a href="https://shape.nato.int/news-releases/nato-acquires-aienabled-warfighting-system-" rel="noopener noreferrer"&gt;Maven&lt;/a&gt; in record time, and the Pentagon brought in &lt;a href="https://www.cnbc.com/2025/07/14/anthropic-google-openai-xai-granted-up-to-200-million-from-dod.html" rel="noopener noreferrer"&gt;top companies&lt;/a&gt; for AI experiments. From "maybe we shouldn't" to "here's $200M"—quite the vibe shift🛡️&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Job shifts happening slowly.&lt;/strong&gt; AI is &lt;a href="https://www.reuters.com/technology/online-marketplace-fiverr-lay-off-30-workforce-ai-push-2025-09-15/" rel="noopener noreferrer"&gt;beginning to affect&lt;/a&gt; some roles (with &lt;a href="https://www.goldmansachs.com/insights/articles/how-will-ai-affect-the-global-workforce" rel="noopener noreferrer"&gt;up to 7% future displacement predicted&lt;/a&gt; by Goldman Sachs), but impact &lt;a href="https://econofact.org/factbrief/fact-check-has-ai-already-caused-some-job-displacement" rel="noopener noreferrer"&gt;remains modest&lt;/a&gt;. Meanwhile, new jobs are emerging: AI ethicists, ML engineers, data specialists…&lt;/p&gt;

&lt;h3&gt;
  
  
  ⚖️ Safety &amp;amp; Ethics
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Make it safe.&lt;/strong&gt; There's strong public interest in alignment (making sure AI does what we want), reliability, and explainability—but budgets remain relatively low.👿&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From "vibe coding" to "vibe hacking".&lt;/strong&gt; AI is already being used for &lt;a href="https://www.anthropic.com/news/disrupting-AI-espionage?_bhlid=e7c247a8feb826c38f8372f10146eafa395ab38a" rel="noopener noreferrer"&gt;cyberattacks&lt;/a&gt;, exploiting code faster than humans can fix it. In parallel, several &lt;a href="https://github.com/aliasrobotics/cai" rel="noopener noreferrer"&gt;projects&lt;/a&gt; and &lt;a href="https://openai.com/index/introducing-aardvark/" rel="noopener noreferrer"&gt;companies&lt;/a&gt; are also exploring AI as security researchers 🔓.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deepfakes and AI-generated video&lt;/strong&gt; are raising new ethical and legal questions, while researches show models can now "&lt;a href="https://www.anthropic.com/research/alignment-faking" rel="noopener noreferrer"&gt;fake alignment&lt;/a&gt;" when being watched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI Model Welfare debates are starting to emerge.&lt;/strong&gt; Researchers and ethicists are now discussing whether AI should merit moral considerations—raising questions about consciousness, sentience, and the ethical treatment. Still early, but more to be expected as systems become increasingly sophisticated. 🤔&lt;/p&gt;

&lt;h3&gt;
  
  
  🔬 AI Beyond Tech
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Chemistry:&lt;/strong&gt; &lt;a href="https://deepmind.google/science/alphafold/" rel="noopener noreferrer"&gt;AlphaFold&lt;/a&gt; and &lt;a href="https://www.profluent.bio/showcase/progen3" rel="noopener noreferrer"&gt;ProGen3&lt;/a&gt; are predicting 3D protein structures and accelerating drug and material discovery.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Healthcare:&lt;/strong&gt; AI is revolutionizing &lt;a href="https://www.aidoc.com/eu/" rel="noopener noreferrer"&gt;diagnostics&lt;/a&gt; and enabling &lt;a href="https://www.tempus.com/" rel="noopener noreferrer"&gt;personalized cancer treatments&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Robotics:&lt;/strong&gt; Major hype around humanoid robots like &lt;a href="https://www.figure.ai/" rel="noopener noreferrer"&gt;Figure 3&lt;/a&gt; and &lt;a href="https://www.tesla.com/en_eu/AI" rel="noopener noreferrer"&gt;Tesla's Optimus&lt;/a&gt; but still far from deployment. Meanwhile, autonomous driving is ramping up fast, with &lt;a href="https://waymo.com/" rel="noopener noreferrer"&gt;Waymo&lt;/a&gt; leading with more than &lt;a href="https://waymo.com/blog/2025/12/2025-year-in-review" rel="noopener noreferrer"&gt;14M rides in 2025&lt;/a&gt; without human drivers.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Other cool stuff:&lt;/strong&gt; AI is now predicting &lt;a href="https://blog.google/technology/google-deepmind/weathernext-2/?utm_source=ai.google&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;weather&lt;/a&gt;, spotting &lt;a href="https://research.google/blog/teaching-gemini-to-spot-exploding-stars-with-just-a-few-examples/?utm_source=ai.google&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;exploding stars&lt;/a&gt;, detecting &lt;a href="https://blog.google/technology/research/first-firesat-images/?utm_source=ai.google&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;wildfires&lt;/a&gt;, and even trying to &lt;a href="https://deepmind.google/models/gemma/dolphingemma/" rel="noopener noreferrer"&gt;understand dolphin language&lt;/a&gt;. 🐬&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🎾 AI AT PLAYTOMIC
&lt;/h2&gt;

&lt;p&gt;💡 &lt;em&gt;How we've been applying these trends:&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🏁 Product Innovation
&lt;/h3&gt;

&lt;p&gt;We've integrated AI into several key areas of our product:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;AI-Driven Customer Care:&lt;/strong&gt; We've deployed super agents that instantly answer questions from clubs and players, significantly improving response times and customer satisfaction.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Personalized Insights:&lt;/strong&gt; Using AI, we've built dashboards for clubs that provide tailored analytics and reports, helping them make better data-driven decisions.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Virtual Buddies:&lt;/strong&gt; AI-generated avatars now help in our educational content, speaking multiple languages and making our product more accessible globally.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Conversational AI:&lt;/strong&gt; We experimented with AI-driven Whatsapp chatbots to explore new ways of user interaction and distribution strategies.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🧑‍💻 Engineering &amp;amp; Operations
&lt;/h3&gt;

&lt;p&gt;AI has transformed our internal workflows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;AI-Powered Development:&lt;/strong&gt; Tools like Cursor and Claude Code have changed how we build, helping us write code faster, plan better architectures, and explore complex codebases.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Operational Automation:&lt;/strong&gt; We've automated repetitive tasks in data analytics, QA processes, and customer support triage, saving hundreds of hours of manual work every month.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Fraud Detection:&lt;/strong&gt; Our AI-powered systems are actively identifying and blocking fraudulent transactions, protecting our ecosystem.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🤔 DID YOU KNOW?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Universal Search:&lt;/strong&gt; Tools like &lt;a href="https://www.notion.com/product/enterprise-search" rel="noopener noreferrer"&gt;Notion AI&lt;/a&gt; now pull information from multiple sources (Slack, GitHub, email), creating a single entry point for knowledge.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Plan Mode:&lt;/strong&gt; The shift towards "spec-first" development—where AI drafts a plan before execution—is leading to more thoughtful architecture and fewer mistakes.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Breaking Silos:&lt;/strong&gt; AI is enabling unprecedented collaboration between engineers and non-engineers, allowing PMs and designers to contribute more directly to the technical process.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📚 WANT TO LEARN MORE?
&lt;/h2&gt;

&lt;p&gt;Here are some fantastic resources to level up your AI game:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;📖 Reading: AI Engineering&lt;/strong&gt; – &lt;a href="https://www.amazon.es/AI-Engineering-Building-Applications-Foundation/dp/1098166302" rel="noopener noreferrer"&gt;Grab this book&lt;/a&gt; if you're ready to go deep into foundation models and AI applications.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;🎬 Watching: The Thinking Game&lt;/strong&gt; – &lt;a href="https://www.youtube.com/watch?v=d95J8yzvjbQ" rel="noopener noreferrer"&gt;This documentary&lt;/a&gt; follows DeepMind's journey from AlphaGo to AlphaFold. It's a must-watch!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Buckle up—2026 is coming in hot! 🚀&lt;/strong&gt; We're not slowing down. The future isn't just arriving—we're building it, one serve at a time. Game on! 🎾⚡&lt;/p&gt;

</description>
      <category>ai</category>
      <category>engineering</category>
      <category>automation</category>
    </item>
    <item>
      <title>Including Figma images in your SwiftUI/Compose Previews</title>
      <dc:creator>Manuel González Villegas</dc:creator>
      <pubDate>Fri, 06 Jun 2025 12:51:55 +0000</pubDate>
      <link>https://forem.com/playtomic/including-figma-images-in-your-swiftuicompose-previews-2kno</link>
      <guid>https://forem.com/playtomic/including-figma-images-in-your-swiftuicompose-previews-2kno</guid>
      <description>&lt;p&gt;A seamless way to integrate your figma designs into your workflow&lt;/p&gt;

&lt;h3&gt;
  
  
  Introduction
&lt;/h3&gt;

&lt;p&gt;During our development process here at Playtomic, we often encounter challenges that lead to frustrating back-and-forth exchanges between developers and designers. One of the main issues is ensuring that the implementation aligns perfectly with the designs, often due to small misalignments like missing or extra spaces.&lt;/p&gt;

&lt;p&gt;To address this, we looked for ways to enhance our workflow and improve collaboration. Figma is the tool our product design team uses to create stunning interfaces, so integrating those designs into our mobile projects can be cumbersome.&lt;/p&gt;

&lt;p&gt;That’s why we created FigmaPreview, a component available for both SwiftUI and Jetpack Compose, allowing us to set an image from Figma as a background directly in the Xcode or Android Studio preview while we code. This simple yet powerful addition to our workflow helps ensure pixel-perfect precision across platforms, significantly reducing the back-and-forth between design and development and allowing us to move faster while maintaining high-quality UI.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡To keep this post clear and avoid repetition, I'll focus on the SwiftUI implementation. However, both the SwiftUI and Jetpack Compose versions share the same API and behave almost identically, with just a few platform-specific differences that I'll cover at the end.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What is FigmaPreview?
&lt;/h3&gt;

&lt;p&gt;FigmaPreview is a custom SwiftUI component that lets you overlay your figma designs directly onto the SwiftUI views, and this background image can serve as a reference, allowing you to make a pixel perfect (or very closely) implementation.&lt;/p&gt;

&lt;p&gt;Before diving into the details, here’s a quick example of how FigmaPreview helps catch subtle misalignments that might go unnoticed at first glance:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjbykx7230bfyu4n4szyo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjbykx7230bfyu4n4szyo.gif" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How to use FigmaPreview
&lt;/h3&gt;

&lt;p&gt;To use FigmaPreview you just need to follow the following simple steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Obtain a token from your account to allow to the component to reach up to your private design files. Obtaining a token is pretty simple, you just need to go to your account settings and click on &lt;code&gt;Generate new token&lt;/code&gt; button&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8xwz27befi3vacg49pis.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8xwz27befi3vacg49pis.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get a link from Figma by clicking on the view that you need to work on&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi8e5ptq8akk93xkqlqh9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi8e5ptq8akk93xkqlqh9.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Wrap your preview view inside of the FigmaPreview component. You only need to invoke the class init from your preview view&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;ExamplePlaytomicView_Previews&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;PreviewProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;previews&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;FigmaPreview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Your figma's url here"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;ExamplePlaytomicView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nv"&gt;firstParam&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"something"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nv"&gt;secondParam&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;47&lt;/span&gt;
                &lt;span class="o"&gt;...&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Now you can adjust the alpha of the figma image that you have in the background of your preview to compare the figma design with your implementation&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2txqrkji93yxg09vv2nf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2txqrkji93yxg09vv2nf.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How we use FigmaPreview
&lt;/h3&gt;

&lt;p&gt;Essentially, we are doing 2 main usages of &lt;code&gt;FigmaPreview&lt;/code&gt; across our development process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coding complete screens&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When building entire screens, we use &lt;code&gt;FigmaPreview&lt;/code&gt; as a background, so we can align our implementation with precision. This ensures that our screens closely matches the intended design from the very beginning, reducing misalignments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxkmxb8k0akz57b39if8g.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxkmxb8k0akz57b39if8g.gif" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Catalog components&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In addition to full screens, we also use &lt;code&gt;FigmaPreview&lt;/code&gt; for individual UI components. To support this, we maintain a separate app within our main project as a component catalog. Our designers can use this app to review and validate UI components, ensuring they meet pixel perfect standards with &lt;code&gt;FigmaPreview&lt;/code&gt; enabled.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi651kdpptauo94z0w69w.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi651kdpptauo94z0w69w.gif" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This approach not only ensures design accuracy at a granular level but also streamlines our workflow, as designers can review, test and approve components directly in the catalog app. By the time the components are added to the main screens, they are already pixel perfect, reducing the need for further adjustments.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it is implemented
&lt;/h3&gt;

&lt;p&gt;The implementation of this component is not very complex, essentially, we just need to transform the url that we obtain from figma into a one that provides you only the image view.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡To obtain the original figma url you can review the second step of the section &lt;code&gt;How to use FigmaPreview&lt;/code&gt; &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To implement this, we use the Figma API, which provides the necessary functionality to access images from Figma files. Here’s a quick breakdown of the process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Extracting the &lt;code&gt;fileId&lt;/code&gt; and &lt;code&gt;nodeId&lt;/code&gt;: First, we parse the URL of the Figma file to obtain two key parameters, the &lt;code&gt;fileId&lt;/code&gt; and the &lt;code&gt;nodeId&lt;/code&gt;. These identifiers specify the file and specific frame or component from which we want to obtain an image.&lt;/li&gt;
&lt;li&gt;Requesting the image from Figma’s API: Now, with the &lt;code&gt;fileId&lt;/code&gt; and &lt;code&gt;nodeId&lt;/code&gt; we can request the image with a simple &lt;code&gt;GET&lt;/code&gt; request to the following endpoint:
&lt;code&gt;https://api.figma.com/v1/images/{fileId}?ids={nodeId}&lt;/code&gt; 
In the request header, we include a custom header named &lt;code&gt;X-Figma-Token&lt;/code&gt; with our figma access token. This returns a URL of the image, which we then use as the base of this component.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;
  Obtaining the image URL
  &lt;p&gt;Here is an example of how to retrieve the Figma image URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    class FigmaPreviewModel: ObservableObject {
        @State private var originalUrl = ""
        @Published var imageUrl: String = ""

        /**
         * Original URL: https://www.figma.com/file/ILeBSptVwqyqpr3gR2KZLh/Improvements-for-switching-teams?type=design&amp;amp;node-id=2012-6765&amp;amp;mode=design&amp;amp;t=60wrWFvQkrJyMHIg-1
         * API URL that needs to be tranformed: https://api.figma.com/v1/images/ILeBApGukqyqprK932KZLh?ids=2099-6965&amp;amp;format=png
         */
        func updateUrl(figmaUrl: String, scale: CGFloat) {
            guard originalUrl != figmaUrl else { return }
            guard
                let url = URL(string: figmaUrl),
                let components = NSURLComponents(url: url, resolvingAgainstBaseURL: false),
                let fileId = components.path?.split(usingRegex: "/").getOrNull(index: 2),
                let nodeId = components.queryItems?.first(where: { $0.name == "node-id" })?.value
            else {
                return
            }
            guard
                let requesrUrl =
                URL(string: "https://api.figma.com/v1/images/\(fileId)?ids=\(nodeId)&amp;amp;format=png&amp;amp;use_absolute_bounds=true&amp;amp;scale=\(scale)")
            else { return }
            var request = URLRequest(url: requesrUrl)
            request.addValue("{your_token_here}", forHTTPHeaderField: "X-Figma-Token")
            let dataTask = URLSession.shared.dataTask(with: request) { data, response, _ in
                guard
                    (response as? HTTPURLResponse)?.statusCode == 200,
                    let data
                else { return }
                DispatchQueue.main.async {
                    guard
                        let decodedPreview = try? JSONDecoder().decode(FigmaPreviewItem.self, from: data),
                        let imageURL = decodedPreview.images?[nodeId.replacingOccurrences(of: "-", with: ":")]
                    else { return }
                    self.imageUrl = imageURL
                    self.originalUrl = figmaUrl
                }
            }
            dataTask.resume()
        }
    }

    struct FigmaPreviewItem: Decodable {
        let err: String?
        let images: [String: String]?
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Android implementation:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        class FigmaPreviewModel {
            private var originalUrl = ""
            var imageUrl by mutableStateOf("")

            /**
             * Original URL: https://www.figma.com/file/ILeBSptVwqyqpr3gR2KZLh/Improvements-for-switching-teams?type=design&amp;amp;node-id=2012-6765&amp;amp;mode=design&amp;amp;t=60wrWFvQkrJyMHIg-1
             * API URL that needs to be tranformed: https://api.figma.com/v1/images/ILeBApGukqyqprK932KZLh?ids=2099-6965&amp;amp;format=png
             */
            @Throws(IOException::class)
            fun updateUrl(figmaUrl: String, density: Float) {
                if (originalUrl == figmaUrl) {
                    return
                }
                val url = URL(figmaUrl)
                val components = url.path.split("/")
                val fileId = components.getOrNull(2) ?: return
                val query = url.query ?: return
                val nodeId = query.split("&amp;amp;").firstOrNull { it.startsWith("node-id=") }?.split("=")?.getOrNull(1) ?: return
                val requestUrl = URL("https://api.figma.com/v1/images/$fileId?ids=$nodeId&amp;amp;format=png&amp;amp;use_absolute_bounds=true&amp;amp;scale=$density")
                val httpURLConnection = requestUrl.openConnection() as HttpURLConnection
                httpURLConnection.apply {
                    requestMethod = "GET"
                    addRequestProperty("X-Figma-Token", "{your_token_here}")
                }
                httpURLConnection.requestMethod = "GET"
                val responseCode = httpURLConnection.responseCode
                if (responseCode != HttpURLConnection.HTTP_OK) {
                    return
                }
                val inputReader = BufferedReader(InputStreamReader(httpURLConnection.inputStream))
                var inputLine: String?
                val response = StringBuffer()
                while (inputReader.readLine().also { inputLine = it } != null) {
                    response.append(inputLine)
                }
                inputReader.close()
                Handler(Looper.getMainLooper()).post {
                    JSONObject(response.toString()).optJSONObject("images")?.optString(nodeId.replace("-", ":"))?.let { imageUrl = it }
                    originalUrl = figmaUrl
                }
            }
        }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;
  Displaying the Figma image
  &lt;p&gt;Once we have the URL, we can create the view to display the image as a background:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    public struct FigmaPreview&amp;lt;Content: View&amp;gt;: View {
        @StateObject var model = FigmaPreviewModel()
        @State private var url: String
        @State private var opacity: Double
        private let content: Content

        public init(
            url: String,
            opacity: Double = 0.4,
            @ViewBuilder content: () -&amp;gt; Content
        ) {
            _url = State(initialValue: url)
            _opacity = State(initialValue: opacity)
            self.content = content()
        }

        public var body: some View {
            @Environment(\.colorScheme) var colorScheme
            ScrollView {
                VStack {
                    HStack {
                        Text("Code")
                        Slider(value: $opacity, in: 0 ... 1)
                            .padding(.horizontal)
                        Text("Figma")
                    }.padding(.horizontal, medium)
                    ZStack(alignment: .top) {
                        content.frame(maxWidth: figmaPreviewDefaultWidth)
                        HStack {
                            let scale = UIScreen.main.scale
                                AsyncImage(url: URL(string: model.imageUrl), scale: scale)
                                .opacity(opacity)
                                .onAppear { model.updateUrl(figmaUrl: url, scale: scale) }
                                .dashedBorder(color: R.color.playtomicSwiftUI.emerald.opacity(0.5))
                        }
                        .ignoresSafeArea(.all)
                        .frame(width: figmaPreviewDefaultWidth)
                        .dashedBorder(color: R.color.playtomicSwiftUI.sky_blue.opacity(0.5))
                    }.frame(minWidth: figmaPreviewDefaultWidth)
                }
            }
        }
    }

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Android implementation:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        val figmaPreviewDefaultWidth = 375.dp

        @Composable
        fun FigmaPreview(url: String, opacity: Float = 0.4f, contentScale: ContentScale = ContentScale.FillBounds, content: @Composable () -&amp;gt; Unit) {
            var opacityState by remember { mutableStateOf(opacity) }
            val model = remember { FigmaPreviewModel() }

            val density = LocalDensity.current.density
            val localContext = LocalContext.current

            LaunchedEffect(url) { Executors.newFixedThreadPool(1).execute { model.updateUrl(url, density = density) } }
            LaunchedEffect(Unit) {
                // mgonzalez: Put here the context that is needed to start any preview
                val contextProvider = object : IContextProvider {
                    override val applicationContext: android.content.Context
                        get() = localContext.applicationContext

                    override val currentActivity: Activity?
                        get() = localContext as? Activity

                    override val activityStack: List&amp;lt;Activity&amp;gt;
                        get() = listOf()
                }
                Context.contextProvider = contextProvider
                PlaytomicUI.initialize(contextProvider = contextProvider)
            }

            Column(verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier) {
                Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = Medium)) {
                    Text("Code")
                    Slider(
                        value = opacityState, onValueChange = { opacityState = it }, valueRange = 0f..1f, modifier = Modifier
                        .padding(horizontal = 16.dp)
                        .weight(1f)
                    )
                    Text("Figma")
                }

                Box(
                    contentAlignment = Alignment.TopCenter,
                    modifier = Modifier
                        .requiredWidth(figmaPreviewDefaultWidth)
                        .dashedBorder(Color.Blue)
                ) {
                    Box(modifier = Modifier.dashedBorder(Color.Green)) { content() }

                    val modifier = if (contentScale == ContentScale.FillBounds) {
                        Modifier
                    } else {
                        Modifier.matchParentSize()
                    }

                    PlaytomicImage(
                        urlString = model.imageUrl,
                        placeholder = com.playtomicui.R.drawable.ic_asset_add_picture_disable,
                        options = ImageOptions.ORIGINAL,
                        contentScale = contentScale,
                        modifier = modifier.alpha(opacityState)
                    )
                }
            }
        }
        ```


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;/p&gt;

&lt;p&gt;This approach simplifies our development process by displaying the figma design right within Xcode. You can see our implementation here: &lt;/p&gt;
&lt;h3&gt;
  
  
  Some limitations that the component have
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Each time you make a change in your code, the component needs to load again the image, what some times might be a bit annoying&lt;/li&gt;
&lt;li&gt;On android, you need to run the preview to see the figmapreview working
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwkgbul7exfk3jyvh4dm0.png" alt=" "&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>figma</category>
      <category>swiftui</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Migrating your cluster to EKS Auto Mode? What You Need to Know Before Taking the Leap</title>
      <dc:creator>Daniel Gonzalez</dc:creator>
      <pubDate>Tue, 04 Feb 2025 08:26:23 +0000</pubDate>
      <link>https://forem.com/playtomic/migrating-your-cluster-to-eks-auto-mode-what-you-need-to-know-before-taking-the-leap-253m</link>
      <guid>https://forem.com/playtomic/migrating-your-cluster-to-eks-auto-mode-what-you-need-to-know-before-taking-the-leap-253m</guid>
      <description>&lt;p&gt;In December 2024, &lt;a href="https://aws.amazon.com/es/about-aws/whats-new/2024/12/amazon-eks-auto-mode/" rel="noopener noreferrer"&gt;AWS introduced Amazon EKS Auto Mode&lt;/a&gt;, a feature designed to simplify Kubernetes cluster management by automating infrastructure provisioning and scaling. We decided to enable it on our existing cluster to test its capabilities, and here’s what we learned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: If you’re already running an EKS cluster with Karpenter and AWS Load Balancer controller, we &lt;strong&gt;don’t recommend&lt;/strong&gt; migrating to EKS Auto Mode. Keep reading to understand why.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fywrammzk3go0dsw0ehwm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fywrammzk3go0dsw0ehwm.png" alt="IA generated image" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Instead of creating a new cluster, we enabled EKS Auto Mode on our development cluster. While activating the feature was straightforward, utilizing its capabilities required &lt;a href="https://docs.aws.amazon.com/eks/latest/userguide/migrate-auto.html" rel="noopener noreferrer"&gt;migrating certain resources&lt;/a&gt;. In this article, we’ll walk you through the steps we took to enable EKS Auto Mode and share our thoughts after using it for a few weeks.&lt;/p&gt;

&lt;p&gt;Let's start by explaining what EKS Auto Mode provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node provisioning, node consolidation, and autoscaling via &lt;a href="https://karpenter.sh/" rel="noopener noreferrer"&gt;Karpenter&lt;/a&gt;,&lt;/li&gt;
&lt;li&gt;Load balancers provisioning through &lt;a href="https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/" rel="noopener noreferrer"&gt;AWS Load Balancer Controller&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Storage provisioning via &lt;a href="https://github.com/kubernetes-sigs/aws-ebs-csi-driver" rel="noopener noreferrer"&gt;Amazon Elastic Block Store (EBS) CSI driver&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Networking and Identity and Access Management come pre-configured in the managed nodes, eliminating the need for cluster add-ons.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F30nfmeezf1ukvjz7w95a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F30nfmeezf1ukvjz7w95a.png" alt="EKS Auto Mode Overview" width="800" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're already using these features, you'll need to &lt;a href="https://docs.aws.amazon.com/eks/latest/userguide/migrate-auto.html" rel="noopener noreferrer"&gt;modify some specifications&lt;/a&gt; in your Kubernetes resources.&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS Load Balancer Controller Migration
&lt;/h3&gt;

&lt;p&gt;Before using Karpenter-managed nodes, migrating the AWS Load Balancer Controller is essential. This is because Instance Metadata Service Version 2 (IMDSv2) isn’t available on these nodes. They are configured with &lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_InstanceMetadataOptionsRequest.html" rel="noopener noreferrer"&gt;HTTPPutResponseHopLimit&lt;/a&gt; = 1, which limits the number of hops the metadata token can travel. This setting cannot be changed, meaning our pods couldn’t use this feature. If you’re using the Datadog agent, this might also impact you.&lt;/p&gt;

&lt;p&gt;Additionally, existing load balancers can’t be transferred to the Auto Mode controller. &lt;a href="https://docs.aws.amazon.com/eks/latest/userguide/migrate-auto.html#load-balancer-migration" rel="noopener noreferrer"&gt;AWS recommends&lt;/a&gt; duplicating them and performing a blue-green deployment. If you’re using DNS records, this change is really simple.&lt;/p&gt;

&lt;h3&gt;
  
  
  Karpenter Migration
&lt;/h3&gt;

&lt;p&gt;Karpenter’s configuration remains mostly unchanged, except for the introduction of a new Custom Resource Definition (CRD) called &lt;a href="https://docs.aws.amazon.com/eks/latest/userguide/create-node-class.html" rel="noopener noreferrer"&gt;NodeClass&lt;/a&gt;. In the case of Karpenter, it uses the &lt;a href="https://karpenter.sh/docs/concepts/nodeclasses/" rel="noopener noreferrer"&gt;EC2NodeClass&lt;/a&gt; object. While AWS provides some documentation, it’s not as comprehensive as Karpenter’s official resources.&lt;/p&gt;

&lt;p&gt;While EKS Auto Mode can automatically create default node pools, we opted to define our own for greater customization. Here's the &lt;code&gt;NodeClass&lt;/code&gt; we created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eks.amazonaws.com/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NodeClass&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pool-default&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ephemeralStorage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;iops&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
    &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;50Gi&lt;/span&gt;
    &lt;span class="na"&gt;throughput&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;125&lt;/span&gt;
  &lt;span class="na"&gt;networkPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DefaultAllow&lt;/span&gt;
  &lt;span class="na"&gt;networkPolicyEventLogs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Disabled&lt;/span&gt;
  &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AmazonEKSAutoNodeRole&lt;/span&gt;
  &lt;span class="na"&gt;securityGroupSelectorTerms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sg-xxxxxxxxxxxxxxxxxx&lt;/span&gt;
  &lt;span class="na"&gt;snatPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Random&lt;/span&gt;
  &lt;span class="na"&gt;subnetSelectorTerms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;subnet-xxxxxxxx&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;subnet-xxxxxxxx&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;subnet-xxxxxxxx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we configured the following &lt;code&gt;NodePool&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;karpenter.sh/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NodePool&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pool-default&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;disruption&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;budgets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;nodes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10%&lt;/span&gt;
    &lt;span class="na"&gt;consolidationPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;WhenUnderutilized&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;node_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;worker&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;expireAfter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;480h&lt;/span&gt;
      &lt;span class="na"&gt;nodeClassRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eks.amazonaws.com&lt;/span&gt;
        &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NodeClass&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pool-default&lt;/span&gt;
      &lt;span class="na"&gt;requirements&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubernetes.io/os&lt;/span&gt;
        &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;In&lt;/span&gt;
        &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;linux&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;karpenter.sh/capacity-type&lt;/span&gt;
        &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;In&lt;/span&gt;
        &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;spot&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;on-demand&lt;/span&gt;
      &lt;span class="c1"&gt;# CPU = 4 CPUs&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eks.amazonaws.com/instance-cpu&lt;/span&gt;
        &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gt&lt;/span&gt;
        &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eks.amazonaws.com/instance-cpu&lt;/span&gt;
        &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Lt&lt;/span&gt;
        &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5"&lt;/span&gt;
      &lt;span class="c1"&gt;# Memory = 16 GiB&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eks.amazonaws.com/instance-memory&lt;/span&gt;
        &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gt&lt;/span&gt;
        &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;16383"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eks.amazonaws.com/instance-memory&lt;/span&gt;
        &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Lt&lt;/span&gt;
        &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;16385"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eks.amazonaws.com/instance-hypervisor&lt;/span&gt;
        &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;In&lt;/span&gt;
        &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nitro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubernetes.io/arch&lt;/span&gt;
        &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;In&lt;/span&gt;
        &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;amd64&lt;/span&gt;
      &lt;span class="na"&gt;terminationGracePeriod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;24h0m0s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During the initial setup, we encountered an issue where the node pool wasn’t reaching a ready state:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs14e1fqd2zw3xbntp6pf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs14e1fqd2zw3xbntp6pf.png" alt="Node pool not ready" width="424" height="70"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After running a describe command on the object, we discovered that dependencies weren’t ready:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg8d77001ayd7g4lete94.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg8d77001ayd7g4lete94.png" alt="Node describe" width="800" height="51"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While we eventually resolved the issue (a misconfiguration in the Node Class role), &lt;strong&gt;I’m sure Karpenter’s logs have the exact details about what went wrong&lt;/strong&gt;, which would have helped us resolve the problem instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;For those setting up a new cluster, EKS Auto Mode is an excellent option. It offers a wide range of features with minimal configuration effort.&lt;/p&gt;

&lt;p&gt;However, if your cluster has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Controllers already configured and running&lt;/li&gt;
&lt;li&gt;Infrastructure as Code (IaC) management in place&lt;/li&gt;
&lt;li&gt;CI/CD pipelines for deploying changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Migrating to EKS Auto Mode might not be worth the effort. While Auto Mode can save &lt;strong&gt;initial setup time&lt;/strong&gt;, it offers minimal advantages once you've already established these capabilities.&lt;/p&gt;

&lt;p&gt;In fact, you might &lt;strong&gt;lose some visibility&lt;/strong&gt;. Currently, logs from Karpenter and other controllers are not accessible in EKS Auto Mode. Adding CloudWatch integration for these logs—similar to Control Plane logs—would be a valuable improvement.&lt;/p&gt;

&lt;p&gt;As AWS enhances EKS Auto Mode with additional features and better tool integration, it may become an attractive option for more use cases. For now, it's best suited for &lt;strong&gt;new clusters&lt;/strong&gt;, while existing setups should carefully weigh the benefits against migration efforts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Call to Action
&lt;/h2&gt;

&lt;p&gt;Have you tried EKS Auto Mode? What are your thoughts? Share your experiences or questions in the comments below!&lt;/p&gt;

</description>
      <category>eks</category>
      <category>kubernetes</category>
      <category>aws</category>
    </item>
    <item>
      <title>Migrating from CocoaPods to Tuist at Playtomic</title>
      <dc:creator>Mohammadreza Koohkan</dc:creator>
      <pubDate>Wed, 08 May 2024 09:27:55 +0000</pubDate>
      <link>https://forem.com/playtomic/migrating-from-cocoapods-to-tuist-at-playtomic-26ed</link>
      <guid>https://forem.com/playtomic/migrating-from-cocoapods-to-tuist-at-playtomic-26ed</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In the constantly changing world of software development, embracing change and innovation is crucial for staying ahead. At Playtomic, we recognized the need for improving our build system and planned to make a big change by migrating our 4 years old dependency management system from "Manual project management” + CocoaPods to Tuist. &lt;/p&gt;

&lt;p&gt;This shift was all about improving the project structure, simplifying development processes, cleaner build configurations, compatibility with Swift Packages and modern SwiftUI projects and giving the overall development experience a huge boost.&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;

&lt;p&gt;Our project lives inside a single git repo and it is divided into different sub projects for different layers of abstraction, following uFeatures architecture in following categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App:&lt;/strong&gt; The core executable module hosting the application, integrating all features, and facilitating integration tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature Modules:&lt;/strong&gt; Modules with code from some particular feature, they are self-contained and all dependencies from other modules are provided through IoC by the coordinators and intent providers. They are normally structured containing coordinators, views, presenters, interactors, models, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared Modules:&lt;/strong&gt; Commonly used modules across features, including foundational components, UI elements, and utility functions. We utilize shared modules to enhance reusability and maintainability.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;For a comprehensive understanding of our project's architecture, you can refer to extensive architecture documentation by Angel Garcia, &lt;strong&gt;&lt;a href="https://dev.to/playtomic/playtomics-shared-architecture-using-swift-and-kotlin-320b"&gt;Playtomic's Shared Architecture using Swift and Kotlin&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Migration Reasons and Initiatives
&lt;/h2&gt;

&lt;p&gt;In the early days of our project back in 2018, we structured our project around CocoaPods dependency manager, the most common dependency management tool back then, but nowadays it is not providing our needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  A. Migration Reasons
&lt;/h3&gt;

&lt;p&gt;CocoaPods was indeed one of the important reasons behind our migration, but it wasn't the only factor. We recognized the need for better dependency management tool as our project growth. We encountered more git conflicts and exhausted long manual setup processes for creating new modules. &lt;/p&gt;

&lt;p&gt;Additionally, updating project settings like the iOS target version became cumbersome across different projects. Furthermore, we encountered several inconsistencies in the configurations of feature modules.&lt;/p&gt;

&lt;p&gt;Here are some of challenges we faced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CocoaPods compatibility issues with SwiftUI and modern swift packages&lt;/li&gt;
&lt;li&gt;CocoaPods was breaking &lt;code&gt;100%&lt;/code&gt; of Xcode SwiftUI Previews&lt;/li&gt;
&lt;li&gt;CocoaPods was causing storyboards loading very slowly (top rated project issue)&lt;/li&gt;
&lt;li&gt;Podfile complexity and dependency maintenance&lt;/li&gt;
&lt;li&gt;Pods folder was stored directly on the repository and not ignored by gitignore, for this reason it did lead to a higher project size&lt;/li&gt;
&lt;li&gt;More git conflicts due to stored xcode project files on the repository&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  B. Migration Goals
&lt;/h3&gt;

&lt;p&gt;After 4 years, in 12th October 2022, we decided to update our build system to Tuist, so we could eliminate main challenges, increase development experience and be ready for future advances.&lt;/p&gt;

&lt;p&gt;We set out to achieve specific goals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Have &lt;code&gt;100%&lt;/code&gt; of SwiftUI previews working&lt;/li&gt;
&lt;li&gt;Have storyboards load in less than &lt;code&gt;2s&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Reduce steps needed to create a new module in iOS by &lt;code&gt;50%&lt;/code&gt; (from 31 to 15)
&lt;em&gt;After finalizing migration we realized that steps reduced from 31 to 10 steps instead!&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In addition to that we were targeting several areas to improve by adopting Tuist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structure and modularity of the project&lt;/strong&gt;: With Tuist we could have a common structure for all of our feature modules, ensuring that they are properly configured and consistent, and save a lot of time declaring and keeping them updated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration from CocoaPods to SPM/Carthage&lt;/strong&gt;: We were struggling to make CocoaPods work properly with the microfeatures architecture, majority of iOS developers community were adopting Swift Package Manager (SPM) or Carthage as main build system, but with Tuist we saw a way to kill 2 birds with 1 stone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build speed improvements&lt;/strong&gt;: The project was becoming bigger and slower to compile. We thought that Tuist could help us improve build time by better modularization and the use of remote and local caches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git Conflicts&lt;/strong&gt;: With more people working in the same project was resulting in frequent git conflicts in the project files. Therefore, we wanted to move to a solution where project files would not be committed but generated, and Tuist provided that (as well as other tools).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keeps the same build tools:&lt;/strong&gt; in contrast with other alternatives, Tuist was not replacing the build system but integrating SPM and Carthage nicely in the existing tools.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Implementation Plan
&lt;/h2&gt;

&lt;p&gt;For replacing Cocoapods with Tuist we took several steps, from creating all the internal modules in Tuist and linking them together&lt;/p&gt;

&lt;p&gt;Key aspects of this transition included:&lt;/p&gt;

&lt;h3&gt;
  
  
  A. Migration Strategy
&lt;/h3&gt;

&lt;p&gt;Our migration to Tuist involved a multi-step strategy, we wanted the integration to be transparent to the team with minimal disruption of the daily workflow of our 16-person team, otherwise it couldn’t be even possible to start. The strategy involved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Transparent Integration:&lt;/strong&gt; Create the Tuist project in a subfolder while maintaining compatibility with CocoaPods during the migration. Developers would continue using the old CocoaPods setup while we work on the migratation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strategic Source Code Linking:&lt;/strong&gt; Link source codes without modifying the existing codebase linked to CocoaPods. This allowed for both build systems to coexist, we had to act drastically to keep both build systems active and buildable until a strategic moment to switch build system from CocoaPods to Tuist&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  B. Migration Steps
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Project Overview (October 2022)
&lt;/h4&gt;

&lt;p&gt;Back in October 2022, we started by listing all of our internal and external dependencies to have an overview In total our project consisted of &lt;strong&gt;&lt;code&gt;20&lt;/code&gt;&lt;/strong&gt; modules, including &lt;strong&gt;&lt;code&gt;1&lt;/code&gt;&lt;/strong&gt; app module with an executable IPA target, &lt;strong&gt;&lt;code&gt;6&lt;/code&gt;&lt;/strong&gt; shared internal dependencies and &lt;strong&gt;&lt;code&gt;13&lt;/code&gt;&lt;/strong&gt; feature modules with dynamic framework targets + unit tests.&lt;/p&gt;

&lt;p&gt;Tuist's flexibility with both SPM and Carthage made this transition from cocoapods much smoother for us, we continued by searching through each external dependency’s git repository to check if it supports Swift Package Manager (SPM) or Carthage by checking following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Supports Swift Package Manager, if git repo contains &lt;code&gt;Package.swift&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;Supports Carthage, If git repo contains an xcode project with schema to build a &lt;code&gt;framework&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While going through this, we found some older libraries, like outdated versions of R.swift, and some Objective-C libraries that didn't support Carthage, Our solution? We either forked and provided requirements or updated to higher versions.&lt;/p&gt;

&lt;h4&gt;
  
  
  Environment Setup and Integration (November 2022)
&lt;/h4&gt;

&lt;p&gt;With more confidence and clarity over the project finally implementation started in early November 2022, we started by following instructions in &lt;a href="https://docs.tuist.io/documentation/tuist/" rel="noopener noreferrer"&gt;Tuist Documentations&lt;/a&gt; and we kicked off migration with Tuist version 3.12.0.&lt;/p&gt;

&lt;p&gt;After we downloaded and installed Tuist command line tool and other required tools like Carthage, we started the migration as follows:&lt;/p&gt;

&lt;h5&gt;
  
  
  1. Creating the Tuist Project
&lt;/h5&gt;

&lt;p&gt;We began migration by initializing tuist project in a sub folder from root of the project at &lt;code&gt;~/playtomic-ios/tuist&lt;/code&gt;, the tuist folder placed besides Pods and Podfile.&lt;/p&gt;

&lt;h5&gt;
  
  
  2. Folder Restructuring
&lt;/h5&gt;

&lt;p&gt;We wanted the migration to be transparent to developers, therefore we didn’t wan’t to move source code that have been already linked to CocoaPods to another folder, we wanted both CocoaPods and Tuist projects coexist even after the migration for some time. &lt;/p&gt;

&lt;p&gt;With the new tuist project in place, we started the tuist migration at a sub folder where only tuist project manifests are located and the actual source code is at the same old location.&lt;/p&gt;

&lt;p&gt;Root of the project playtomic-ios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pods&lt;/li&gt;
&lt;li&gt;Podfile&lt;/li&gt;
&lt;li&gt;playtomic.xcworkspace (OLD)&lt;/li&gt;
&lt;li&gt;App → Source code only&lt;/li&gt;
&lt;li&gt;Shared → Source code only&lt;/li&gt;
&lt;li&gt;Features/

&lt;ul&gt;
&lt;li&gt;Feature-A → Source code only&lt;/li&gt;
&lt;li&gt;rest of features… → Source code only&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;tuist/

&lt;ul&gt;
&lt;li&gt;playtomic.xcworkspace (NEW generated by tuist)&lt;/li&gt;
&lt;li&gt;Workspace.swift&lt;/li&gt;
&lt;li&gt;App → Manifest only&lt;/li&gt;
&lt;li&gt;Shared → Manifest only&lt;/li&gt;
&lt;li&gt;Features/&lt;/li&gt;
&lt;li&gt;Feature-A → Manifest only&lt;/li&gt;
&lt;li&gt;rest of features… → Manifest only&lt;/li&gt;
&lt;li&gt;Tuist/… (New project settings and dependencies)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h5&gt;
  
  
  3. Create Project.swift manifest file for each module
&lt;/h5&gt;

&lt;p&gt;Project.swift manifest represents an Xcode project. It’s used to define the targets of the project and their dependencies. For us Project.swift file represents implementation of each internal module we have.&lt;/p&gt;

&lt;p&gt;For example the Project.swift file for a shared module like “Mozart” looks like this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;

&lt;span class="c1"&gt;// playtomic-ios/tuist/Shared/Mozart/Project.swift&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Mozart"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;organizationName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Playtomic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="kt"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Mozart"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nv"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iOS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nv"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;framework&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nv"&gt;bundleId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"com.playtomic.Mozart"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// Pointing to the source code folder -&amp;gt; playtomic-ios/Mozart/Mozart/...&lt;/span&gt;
      &lt;span class="nv"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"../../../Mozart/Mozart/**/*.swift"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
      &lt;span class="c1"&gt;// Pointing to the resources folder where assets are placed -&amp;gt; playtomic-ios/Mozart/Mozart/...&lt;/span&gt;
      &lt;span class="nv"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"../../../Mozart/Mozart/**/{*.strings,*.xcassets,*.storyboard,*.xib}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nv"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="c1"&gt;// Internal or External dependencies will be linked here &lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="kt"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Mozart UnitTests"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nv"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iOS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nv"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unitTests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nv"&gt;bundleId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"com.playtomic.Mozart.UnitTests"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// Pointing to the Unit Tests folder source code -&amp;gt; playtomic-ios/Mozart/MozartTests/...&lt;/span&gt;
      &lt;span class="nv"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"../../../Mozart/MozartTests/**/*.swift"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// Pointing to Unit Tests resources -&amp;gt; playtomic-ios/Mozart/MoartTests/...&lt;/span&gt;
      &lt;span class="nv"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"../../../Mozart/MozartTests/**/{*.strings,*.xcassets,*.storyboard,*.xib}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nv"&gt;dependencies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Mozart"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;// Internal or External dependencies will be linked here&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;In the migration process Project.swift files play a crucial role in defining the manifest of internal modules. These files are strategically located to be ensure a transparent migration. &lt;/p&gt;

&lt;p&gt;As an example the &lt;code&gt;playtomic-ios/tuist/Shared/Mozart/Project.swift&lt;/code&gt; only defines Mozart module manifest and for the source code manifest will point to the same source files utilized in our CocoaPods project &lt;code&gt;playtomic-ios/Shared/Mozart/...&lt;/code&gt;, this approach allowed us to execute the migration while ensuring both Tuist and CocoaPods projects are working seamlessly.&lt;/p&gt;

&lt;h5&gt;
  
  
  4. Setting up build schemes
&lt;/h5&gt;

&lt;p&gt;Schema for each target will be generated automatically by tuist, and in addition to that Tuist automatically generates an umbrella schema with every internal target listed for building and every test target listed for testing, if you don’t want the umbrella schema you can disable it by passing false to &lt;code&gt;autogeneratedWorkspaceSchemes&lt;/code&gt; in the Workspace.swift manifest file.&lt;/p&gt;

&lt;h5&gt;
  
  
  5. Setting up different language and localization
&lt;/h5&gt;

&lt;p&gt;We simplified this process by passing a glob pattern for resources that also searches for string tables, it efficiently collects files ending with the &lt;strong&gt;&lt;code&gt;.strings&lt;/code&gt;&lt;/strong&gt; extension, this pattern is passed to the resource arguments in Project.swift file as follows:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;During Xcode project generation, any collected &lt;strong&gt;.strings&lt;/strong&gt; file will be linked to the project&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;

&lt;span class="c1"&gt;// playtomic-ios/tuist/App/Project.swift&lt;/span&gt;

&lt;span class="kt"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="c1"&gt;// Pointing to resources -&amp;gt; playtomic-ios/App/Playtomic/...&lt;/span&gt;
  &lt;span class="nv"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"../../../App/Playtomic/**/{*.strings,*.xcassets,*.storyboard,*.xib}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h5&gt;
  
  
  6. Grouping Project.swift files under a Workspace.swift
&lt;/h5&gt;

&lt;p&gt;This manifest represents an Xcode workspace. An Xcode Workspace is used to group other projects and add additional files and schemas, it has access to every added project’s targets.&lt;/p&gt;

&lt;p&gt;Once you define Workspace.swift file within tuist manifest folder, it auto-generates a workspace file.&lt;/p&gt;

&lt;p&gt;In our project, we were looking for flexibility in customizing project schemas and enable or disable specific unit test targets as needed, such as integration tests. The final Workspace.swift file, handling grouping projects with customized schemas, looks like this:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;

&lt;span class="c1"&gt;// playtomic-ios/tuist/Workspace.swift&lt;/span&gt;

&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;Foundation&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;ProjectDescription&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;ProjectDescriptionHelpers&lt;/span&gt;

&lt;span class="c1"&gt;// Assigned with Production build configuration&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;productionScheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Scheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Playtomic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Assigned with Development build configuration&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;integrationScheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Scheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Playtomic (Integration)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;workspace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Workspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Playtomic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s"&gt;"App/Project.swift"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"Shared/Mozart/Project.swift"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Other Shared modules...&lt;/span&gt;
    &lt;span class="s"&gt;"Features/Onboarding/Project.swift"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Other Feature modules...&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nv"&gt;schemes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;productionScheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;integrationScheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nv"&gt;generationOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Workspace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;GenerationOptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;autogeneratedWorkspaceSchemes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;disabled&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h5&gt;
  
  
  7. Tuist generate and first run
&lt;/h5&gt;

&lt;p&gt;With this added to the tuist manifest project, one final command line execution of the &lt;code&gt;tuist generate&lt;/code&gt;, creates and opens the workspace, one final run through iOS Simulators and, BOOM, Success!&lt;/p&gt;


&lt;h2&gt;
  
  
  Preparing for the Merge: Transitioning Code into the Development Branch
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExY2VvMjlrNWptaGV2ZDM4ODVwdGQ5aGUxYnJ2YmJldGJsZTZiN2FtMyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/cFkiFMDg3iFoI/giphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExY2VvMjlrNWptaGV2ZDM4ODVwdGQ5aGUxYnJ2YmJldGJsZTZiN2FtMyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/cFkiFMDg3iFoI/giphy.gif" alt="GIT-MERGE-GIF"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Transitioning from CocoaPods to Tuist wasn't a smooth sail. Here's how we tackled development experience gap between CocoaPods and Tuist.&lt;/p&gt;
&lt;h3&gt;
  
  
  A. Installation of the new tools
&lt;/h3&gt;

&lt;p&gt;Getting started with Tuist meant installing new tools with specific versions. To streamline this process, we detailed comprehensive instructions in our README file. Developers could easily follow these steps to install the necessary tools such as Tuist and Carthage, fetch third-party libraries, and generate the new workspace.&lt;/p&gt;
&lt;h3&gt;
  
  
  B. Bridging the Gap: Daily Work Differences
&lt;/h3&gt;

&lt;p&gt;We wanted the migration to be transparent and improve the development experience, but the nature of Tuist and CocoaPods was so different. We used to keep third party dependencies on the project repository under the Pods folder, but with tuist we decided to not keep third party library caches of SPM and Carthage on the repository.&lt;/p&gt;

&lt;p&gt;In addition to that, Tuist required developers to execute the &lt;strong&gt;&lt;code&gt;tuist generate&lt;/code&gt;&lt;/strong&gt; command to update and generate the &lt;strong&gt;&lt;code&gt;.xcworkspace&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;.xcodeproj&lt;/code&gt;&lt;/strong&gt; files. Since these files are ignored by gitignore and changes made to the workspace file didn't reflect on others' devices.&lt;/p&gt;

&lt;p&gt;To maintain a seamless transition, we aimed to keep developers workflow similar to CocoaPods. We didn't want them to manually run the &lt;strong&gt;&lt;code&gt;tuist generate&lt;/code&gt;&lt;/strong&gt; every time they switch branches. Instead, we wanted them to open the &lt;strong&gt;&lt;code&gt;.xcworkspace&lt;/code&gt;&lt;/strong&gt; file and dive into coding.&lt;/p&gt;

&lt;p&gt;To achieve this, we leveraged git hooks as trigger points. By adding bash scripts to &lt;code&gt;post-checkout&lt;/code&gt; and &lt;code&gt;post-merge&lt;/code&gt; git hooks, we ensured that every time developers interacted with the project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Third-party dependencies were updated as needed&lt;/li&gt;
&lt;li&gt;Workspace file was consistently updated on their repositories.&lt;/li&gt;
&lt;li&gt;Project will be opened automatically with the updated files and third-party repositories&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; 
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Press control ^ + C to cancel git hook execution"&lt;/span&gt; 
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; 

&lt;span class="nv"&gt;TUIST_MANIFEST_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;".../playtomic-ios/tuist"&lt;/span&gt; 

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Fetch and update third party dependencies"&lt;/span&gt;
tuist fetch &lt;span class="nt"&gt;--update&lt;/span&gt; &lt;span class="nt"&gt;--path&lt;/span&gt; TUIST_MANIFEST_PATH 

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Generate and open project workspace"&lt;/span&gt; 
tuist generate &lt;span class="nt"&gt;--path&lt;/span&gt; TUIST_MANIFEST_PATH


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;h2&gt;
  
  
  Experimental Phase: Challenges we faced and how we overcame it
&lt;/h2&gt;

&lt;p&gt;After finalizing Tuist migration implementation and merging changes with the codebase in December 2022, we began the experimental phase. During this period, we built the project on several Mac machines, identifying and addressing the following issues:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Duplicate Static Dependencies
&lt;/h3&gt;

&lt;p&gt;When we used &lt;strong&gt;&lt;code&gt;tuist generate&lt;/code&gt;&lt;/strong&gt; command for generating the Tuist project, we encountered hundreds of warnings displayed on the Terminal about duplicated dependencies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Warning: Target 'Tournament' has duplicate project dependency specified: 'GoogleSignIn'&lt;/li&gt;
&lt;li&gt;Warning: Target 'Academy' has duplicate project dependency specified: 'GoogleSignIn'&lt;/li&gt;
&lt;li&gt;Warning: Target 'Location' has duplicate project dependency specified: 'GoogleSignIn'&lt;/li&gt;
&lt;li&gt;Warning: Target 'GoogleSignIn' has been linked from target 'Onboarding', target 'Payment', target 'Paywall', etc.., it is a static product so may introduce unwanted side effects.&lt;/li&gt;
&lt;li&gt;etc..&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To address this, we changed the linking from static to dynamic frameworks. For dependencies imported by a single module, we kept them statically linked as it was not causing any duplication issue. However, for dependencies imported by multiple internal modules, especially those with transitive libraries, we switched to dynamic linking.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;GoogleSignIn&lt;/code&gt; had some libraries inside such as &lt;code&gt;AppAuthCore&lt;/code&gt; which was also imported by other google products like Firebase, this means that our app is indirectly linked those frameworks. The configuration in our Tuist &lt;code&gt;Dependencies.swift&lt;/code&gt; file better explains this approach:&lt;/p&gt;

&lt;p&gt;The configuration in our Tuist &lt;strong&gt;&lt;code&gt;Dependencies.swift&lt;/code&gt;&lt;/strong&gt; file describes this approach:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6l7lhp3moim4lacvgzkt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6l7lhp3moim4lacvgzkt.png" alt="Dependencies-Image"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Disabling Tuist's Automatic Code Generation
&lt;/h3&gt;

&lt;p&gt;Since we were using R.swift instead of SwiftGen for generating resources with references in code, we disabled Tuist's automatic code generation tool.&lt;/p&gt;

&lt;p&gt;If you don’t want to enable resource synthesizers, you can just pass empty array &lt;code&gt;resourceSynthesizers: []&lt;/code&gt; in &lt;code&gt;Project&lt;/code&gt; initializer.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;

&lt;span class="kt"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;resourceSynthesizers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This code snippet will disable the auto-generation of resources while still allowing to continue other magical automatic processes, like generating Info.plists and etc.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Reduce New Feature Module Creation Steps
&lt;/h3&gt;

&lt;p&gt;There is a tool in Tuist called &lt;code&gt;scaffold&lt;/code&gt;, it helps you bootstrap new components from .stencil templates that you defined, these templates are consistent with your project.&lt;/p&gt;

&lt;p&gt;We did use it to create a new feature module called Level to Playtomic:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

tuist scaffold mvi-module &lt;span class="nt"&gt;--name&lt;/span&gt; Level


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;For example a file called Presenter.stencil will be used as template, arguments passed in scaffold command like name as &lt;code&gt;—name Level&lt;/code&gt; will be replaced by &lt;code&gt;{{ name }}&lt;/code&gt; in the stencil file.&lt;/p&gt;

&lt;p&gt;Therefore this method enables us to create several files at different locations to generate an entire feature module with just a single command.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6hjq79g4yhxuax86dsse.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6hjq79g4yhxuax86dsse.png" alt="Presenter-stencil-image"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Performance Metrics and Optimization
&lt;/h2&gt;

&lt;p&gt;To determine the effectiveness of the migration, we explored key performance metrics, comparing the new system with the old one. Our focus was on improving the app launch time and reducing the size of the final binary as decent goals.&lt;/p&gt;

&lt;h3&gt;
  
  
  App Launch Time
&lt;/h3&gt;

&lt;p&gt;We have analyzed app launch time by utilizing Firebase Performance tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Tuist IPA&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  Initializing for first time: 1.08s&lt;/li&gt;
&lt;li&gt;  AppDelegate - didFinishLaunchingWithOptions: 414ms&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;  &lt;strong&gt;CocoaPods IPA&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  Initializing for first time: 2.03s&lt;/li&gt;
&lt;li&gt;  AppDelegate - didFinishLaunchingWithOptions: 671ms&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;These results highlight a notable enhancement in the initialization phase, we were so happy because this improvement was not our primary goal but something nice to have!&lt;/p&gt;

&lt;h3&gt;
  
  
  Size of Final Binary
&lt;/h3&gt;

&lt;p&gt;The transition to Tuist allowed us to create cleaner and less duplicated target configurations, resulting in a more streamlined and efficient project structure. This migration contributed to a smaller binary size:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;CocoaPods IPA:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  Total Size of the IPA: 107.575.065 bytes (107.6 MB)&lt;/li&gt;
&lt;li&gt;  Payload: 167.386.848 bytes (167.4 MB)&lt;/li&gt;
&lt;li&gt;  App on AppStore: 164.9 MB&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;  &lt;strong&gt;Tuist IPA:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  Total Size of the IPA: 99.723.283 bytes (99.7 MB)&lt;/li&gt;
&lt;li&gt;  Payload: 140.351.007 bytes (140.4 MB)&lt;/li&gt;
&lt;li&gt;  App on AppStore: (138.7 MB)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Build Compilation Time on M1 Pro with 16GB RAM
&lt;/h3&gt;

&lt;p&gt;Additionally, we conducted an in-depth analysis of the build compilation time on an M1 Pro with 16GB RAM. The results, comparing CocoaPods and Tuist, are as follows:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CocoaPods:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Xcode Clean: 2.9s&lt;/li&gt;
&lt;li&gt;  Xcode Build Playtomic DEBUG: 189s&lt;/li&gt;
&lt;li&gt;  Xcode Build Playtomic RELEASE: 196s&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tuist:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Xcode Clean: 5.9s&lt;/li&gt;
&lt;li&gt;  Xcode Build Playtomic DEBUG: 163s&lt;/li&gt;
&lt;li&gt;  Xcode Build Playtomic RELEASE: 170s&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Further optimizations were achieved by leveraging Tuist's caching capabilities. With &lt;strong&gt;&lt;code&gt;tuist cache warm&lt;/code&gt;&lt;/strong&gt; that pre-compiles modules. We’ve particularly utilized tuist cache for third party libraries and we observed reduced build time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Xcode Clean: 2.6s (&lt;em&gt;2.25x Faster&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;  Xcode Build Playtomic DEBUG: 143s (&lt;em&gt;1.13x Faster&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;  Xcode Build Playtomic RELEASE: 154s (&lt;em&gt;1.1x Faster&lt;/em&gt;)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  CI Improvements
&lt;/h2&gt;

&lt;p&gt;In the final phase of our transition, we had to move all the scripts we used before. These scripts were crucial for different tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Running Unit Tests: Essential for checking if our code changes were good to go before merging them (you know, the whole Pull Request approval thing).&lt;/li&gt;
&lt;li&gt;  Generating Testing Binaries: This helped us create versions of our app specifically for review and Quality Assurance (QA) testing.&lt;/li&gt;
&lt;li&gt;  Releasing to the AppStore: The process of actually shipping our app to the big wide world. This includes submitting all the necessary information and metadata.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We didn't just stop at improving the app build, we also fine-tuned our Continuous Integration processes on Bitrise our CI provider.&lt;/p&gt;

&lt;p&gt;Moving from CocoaPods to Tuist wasn't just about building and testing; it was about making everything faster. We used Tuist magic for pre-compiling projects &lt;strong&gt;&lt;code&gt;tuist cache warm&lt;/code&gt;&lt;/strong&gt; and Bitrise &lt;strong&gt;&lt;code&gt;key-based&lt;/code&gt;&lt;/strong&gt; caching for third-party Swift Package Manager (SPM) and Carthage libraries source codes.&lt;/p&gt;

&lt;p&gt;As outlined before, we used to have the Pods folder on the project repository for the same purpose of not downloading dependencies on CI machine and avoid execution of &lt;code&gt;**pod** **install**&lt;/code&gt; on every CI job.&lt;/p&gt;

&lt;p&gt;With Bitrise &lt;strong&gt;&lt;code&gt;key-based&lt;/code&gt;&lt;/strong&gt; caching, we fetched third-party libraries, including their source code, only once and later efficiently retrieved them using Bitrise's caching mechanism. This resembles the approach of storing the Pods folder but with the added benefits of reduced repository size and improved efficiency. Users can configure caching to create new cache archives periodically, since cache archive remains valid for seven days but resetting if updated and discarding old ones automatically. &lt;/p&gt;

&lt;p&gt;Without &lt;strong&gt;&lt;code&gt;key-based&lt;/code&gt;&lt;/strong&gt; caching, fetching third-party dependencies for SPM and Carthage projects incurred unnecessary overhead on every CI job. Now, we fetch once, update the key based on new hashes generated from Package.resolved and Carthage.resolved files, mirroring the concept of CocoaPods' Podfile.lock CHECKSUM value. This streamlining significantly improves the efficiency of our CI pipelines.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion: Reflecting on Our Journey
&lt;/h2&gt;

&lt;p&gt;Migrating from CocoaPods to Tuist was a strategic move that has significantly benefited Playtomic. Not only did we overcome the limitations posed by CocoaPods, but we also achieved a more streamlined, efficient, and future-ready development environment.&lt;/p&gt;

&lt;p&gt;This success was accomplished by careful planning and a focus on minimizing disruption for other developers.&lt;/p&gt;

&lt;p&gt;As we look back on this transition, we're reminded of the importance of adaptability in technology. By embracing Tuist, we've not only enhanced our current development system but also positioned ourselves for future growth and advancement.&lt;/p&gt;

&lt;p&gt;This experience underscores our commitment to continuous improvement and our dedication to delivering the best possible experience to our users and developers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://docs.tuist.io/documentation/tuist/" rel="noopener noreferrer"&gt;Tuist Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://devcenter.bitrise.io/en/dependencies-and-caching/key-based-caching.html" rel="noopener noreferrer"&gt;Bitrise Key-Based Caching&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://dev.to/playtomic/playtomics-shared-architecture-using-swift-and-kotlin-320b"&gt;Playtomic's Shared Architecture using Swift and Kotlin&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>tuist</category>
      <category>ios</category>
      <category>cocoapods</category>
      <category>swift</category>
    </item>
    <item>
      <title>Adding multi device previews in Xcode and Android Studio</title>
      <dc:creator>Angel G. Olloqui</dc:creator>
      <pubDate>Wed, 05 Apr 2023 09:48:47 +0000</pubDate>
      <link>https://forem.com/playtomic/adding-multi-device-previews-in-xcode-and-android-studio-4cc4</link>
      <guid>https://forem.com/playtomic/adding-multi-device-previews-in-xcode-and-android-studio-4cc4</guid>
      <description>&lt;p&gt;Developing with SwiftUI and Jetpack Compose is very user-friendly. Both frameworks offer a highly readable declarative syntax and a live preview feature that updates in real-time while coding. This live preview allows developers to easily configure various device settings, although it can be a bit cumbersome to specify multiple device factors. At Playtomic, we have created a simple utility for both platforms that allows us to run our previews on a predefined set of devices. Here's what we did:&lt;/p&gt;

&lt;h2&gt;
  
  
  Android
&lt;/h2&gt;

&lt;p&gt;This platform was the easy one, as it already supports aggregated annotations out of the box. All you need to do is include somewhere in your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Preview(showSystemUi = true, device = Devices.PIXEL_4_XL, name = "Pixel 4 XL")   // big xxxhdpi
@Preview(showSystemUi = true, device = Devices.NEXUS_5, name = "Nexus 5")   // small-medium xxhdpi
annotation class MultiDevicePreview
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then just replace the usage of &lt;code&gt;@Preview&lt;/code&gt; by &lt;code&gt;@MultiDevicePreview&lt;/code&gt; in your previews:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@MultiDevicePreview
@Composable
private fun AMultiDevicePreview() {
    Text("Multidevice preview")
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--0kCh8AeF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/73578b7e3mdi5qeyosji.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0kCh8AeF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/73578b7e3mdi5qeyosji.png" alt="Screenshot from preview in Android Studio using multiple device previews" width="603" height="648"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  iOS
&lt;/h2&gt;

&lt;p&gt;Things become a bit more complicated for iOS. Firstly, Xcode only recognizes previews that implement the &lt;code&gt;PreviewProvider&lt;/code&gt; protocol. As a result, extending the interface with our custom version of &lt;code&gt;MultiDevicePreviewProvider&lt;/code&gt; doesn't work. Instead, we created a second protocol to tackle this problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public protocol MultiDevicePreview {
    associatedtype DevicePreviews : View
    @ViewBuilder @MainActor static var devicePreviews: DevicePreviews { get }

    @MainActor static var devices: [PreviewDevice] { get }
    @MainActor static var previewName: String? { get }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then, we provided a default implementation of the previews for those using the new protocol:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public extension PreviewProvider where Self: MultiDevicePreview {
    static var previews: some View {
        ForEach(devices) { device in
            AnyView(devicePreviews)
                .previewDevice(device)
                .previewDisplayName([previewName, device.rawValue].compactMap { $0 }.joined(separator: " - "))
        }
    }

    static var devices: [PreviewDevice] { PreviewDevice.allCases }
    static var previewName: String? { String(describing: Self.self) }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also declared some predefined list of devices to simplify the management:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;extension PreviewDevice {
    public static let iPhone14 = PreviewDevice("iPhone 14")
    public static let iPhone14Max = PreviewDevice("iPhone 14 Pro Max")
    public static let iPhoneSE = PreviewDevice("iPhone SE (3rd generation)")
    public static let allCases = [iPhone14, iPhone14Max, iPhoneSE]
}

extension PreviewDevice: Identifiable {
    public var id: String {
        rawValue
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With all of the above, you can make use of multidevice previews by conforming your preview to &lt;code&gt;MultiDevicePreview&lt;/code&gt; and replace the method &lt;code&gt;previews&lt;/code&gt; by &lt;code&gt;devicePreviews&lt;/code&gt; like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;struct AMultiDevicePreview: PreviewProvider, MultiDevicePreview {
    static var devicePreviews: some View {
        Text("multidevice preview text")
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VB0BicC0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kpzlxakk41vj7881fln6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VB0BicC0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kpzlxakk41vj7881fln6.png" alt="Scheenshot from Xcode using multiple device previews" width="880" height="778"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not a huge difference, but a nice small addition to the toolkit!&lt;/p&gt;

</description>
      <category>ios</category>
      <category>android</category>
      <category>swiftui</category>
      <category>compose</category>
    </item>
    <item>
      <title>Data Races with value types in Swift</title>
      <dc:creator>Angel G. Olloqui</dc:creator>
      <pubDate>Fri, 26 Aug 2022 14:49:54 +0000</pubDate>
      <link>https://forem.com/playtomic/data-races-with-value-types-in-swift-5e45</link>
      <guid>https://forem.com/playtomic/data-races-with-value-types-in-swift-5e45</guid>
      <description>&lt;p&gt;This week I had an &lt;a href="https://github.com/GetStream/swift-activity-feed/issues/19"&gt;interesting discussion around a possible data race condition&lt;/a&gt; due to wrong threading synchronisation when manipulating a value type (a &lt;code&gt;String&lt;/code&gt; in this case) in a class. &lt;/p&gt;

&lt;h2&gt;
  
  
  The buggy code
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;final class MyClass {
    var token: String

    init(_ token: String = "") {
        self.token = token
    }

    func myMethod() -&amp;gt; Bool {
        token.isEmpty
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a first look, this might seem correct. We have just a &lt;code&gt;var&lt;/code&gt; with a &lt;code&gt;String&lt;/code&gt;, which is a value type, and a method to just check if the &lt;code&gt;token&lt;/code&gt; is empty that only calls the &lt;code&gt;isEmpty&lt;/code&gt; from &lt;code&gt;String&lt;/code&gt;. Straightforward code and safe right? well, it is OK as long as you do not introduce threading, but the moment you do it will not. Let me elaborate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test
&lt;/h2&gt;

&lt;p&gt;If you run this test with Thread Sanitizer enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; func test_data_race() {
        let sut = MyClass()

        DispatchQueue.concurrentPerform(iterations: 1_000_000) { i in
            sut.token = "\(i)"
            _ = sut.myMethod()
        }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you will see this output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WARNING: ThreadSanitizer: data race (pid=8329)
  Read of size 8 at 0x000107c1aab8 by thread T2:
    #0 closure #1 in DataTests.test_data_race() DataTests.swift:69 (Tests:arm64+0xde354)
    #1 partial apply for closure #1 in DataTests.test_data_race() &amp;lt;compiler-generated&amp;gt; (Tests:arm64+0xde3e4)
    #2 partial apply for thunk for @callee_guaranteed (@unowned Int) -&amp;gt; () &amp;lt;null&amp;gt;:73675156 (libswiftDispatch.dylib:arm64+0x42f4)
    #3 _dispatch_client_callout2 &amp;lt;null&amp;gt;:73675156 (libdispatch.dylib:arm64+0x35dc)

  Previous write of size 8 at 0x000107c1aab8 by main thread:
    #0 closure #1 in DataTests.test_data_race() DataTests.swift:69 (Tests:arm64+0xde374)
    #1 partial apply for closure #1 in DataTests.test_data_race() &amp;lt;compiler-generated&amp;gt; (Tests:arm64+0xde3e4)
    #2 partial apply for thunk for @callee_guaranteed (@unowned Int) -&amp;gt; () &amp;lt;null&amp;gt;:73675156 (libswiftDispatch.dylib:arm64+0x42f4)
    #3 _dispatch_client_callout2 &amp;lt;null&amp;gt;:73675156 (libdispatch.dylib:arm64+0x35dc)
    #4 _swift_dispatch_apply_current &amp;lt;null&amp;gt;:73675156 (libswiftDispatch.dylib:arm64+0x43a0)
    #5 @objc DataTests.test_data_race() &amp;lt;compiler-generated&amp;gt; (Tests:arm64+0xde448)
    #6 __invoking___ &amp;lt;null&amp;gt;:73675156 (CoreFoundation:arm64+0x11c5ec)

  Location is heap block of size 32 at 0x000107c1aaa0 allocated by main thread:
    #0 __sanitizer_mz_malloc &amp;lt;null&amp;gt;:73675156 (libclang_rt.tsan_iossim_dynamic.dylib:arm64+0x51004)
    #1 _malloc_zone_malloc &amp;lt;null&amp;gt;:73675156 (libsystem_malloc.dylib:arm64+0x1527c)
    #2 DataTests.test_data_race() DataTests.swift:66 (Tests:arm64+0xde07c)
    #3 @objc DataTests.test_data_race() &amp;lt;compiler-generated&amp;gt; (Tests:arm64+0xde448)
    #4 __invoking___ &amp;lt;null&amp;gt;:73675156 (CoreFoundation:arm64+0x11c5ec)

  Thread T2 (tid=6246748, running) is a GCD worker thread

SUMMARY: ThreadSanitizer: data race DataTests.swift:69 in closure #1 in DataTests.test_data_race()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So ThreadSanitizer is detecting a &lt;strong&gt;data race in the code when accessing the token&lt;/strong&gt;. &lt;br&gt;
What does it mean? basically you are making a wrong usage of the variable. It gets read and write operations concurrently but the variable itself is not protected, and the fact that it is a value type does not help.&lt;/p&gt;

&lt;p&gt;What can this cause? it is undefined, but in practice most likely you will have a crash when compilation optimizations are enabled.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;OK, so this simple code can crash when reading and writing the token in parallel from different threads! How can we fix it? we just need to make serial access to read/write. There are multiple ways of doing it (with different primitives), but this could be one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;final class MyClass {
    private let syncQueue = DispatchQueue(label: "com.test.myQueue", attributes: .concurrent)
    private var _token: String
    var token: String {
        get {
            syncQueue.sync {
                _token
            }
        }
        set {
            syncQueue.async(flags: .barrier) {
                _token = newValue
            }
        }
    }

    init(_ token: String = "") {
        _token = token
    }

    func myMethod() -&amp;gt; Bool {
        token.isEmpty
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, what we did is to protect the &lt;code&gt;var&lt;/code&gt; by forcing serial writing to it, so multiple reading can happen but only 1 thread can execute a write at a time (the barrier waits for all previous readings to finish and postpones all subsequent read/write accesses till the write is done). The resulting code is slower to execute, but it is now safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;I wanted to share a few thoughts around this issue, that are common misconceptions in the Swift community:&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ Value types are thread safe
&lt;/h3&gt;

&lt;p&gt;Since the value type has a copy semantic, it may seem logical to think that they are inherently protected from data races. However, that is not the case. &lt;strong&gt;Swift does not guarantee thread safety in value types&lt;/strong&gt;, so accessing any &lt;code&gt;var&lt;/code&gt; from multiple threads is a potential data race condition. This issue of course does not apply to &lt;code&gt;let&lt;/code&gt; variables since they are immutable.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ Value types are always copied
&lt;/h3&gt;

&lt;p&gt;That is the semantic but not really what happens under the hood. When passing value types around, Swift compiler is smart enough to know if the copy is needed, removing unnecessary copies. In practice it uses a CopyOnWrite(COW) strategy, where it will &lt;strong&gt;make the copy only when the value is modified&lt;/strong&gt;, but not when passed around. As a result, in most situations you will actually have a pointer to the same underlaying memory address even when using value types.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ Tests do always behave like production code
&lt;/h3&gt;

&lt;p&gt;The fact that a test does not crash is no guarantee to assert that some code can not crash in production. Tests run in simulated environments and &lt;strong&gt;they normally have different compilation options than the ones in your final builds&lt;/strong&gt;. For example, ARC will make aggressive optimizations when compiling with the proper options, so lots of unnecessary retain/releases will be removed from final builds. In this particular case, my test suit was not crashing, and I was only able to see some wrong usage by activating the Thread Sanitizer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extra reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://forums.swift.org/t/understanding-swifts-value-type-thread-safety/41406"&gt;Understanding Swift's value types thread safety&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://angelolloqui.com/blog/21-ARC-I-Introduction-to-ARC-and-how-it-works-internally"&gt;ARC optimizations&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>swift</category>
      <category>ios</category>
      <category>datarace</category>
      <category>bugs</category>
    </item>
    <item>
      <title>Modernizing a CI+CD pipeline with Github Actions</title>
      <dc:creator>Sergio Garcia Moratilla</dc:creator>
      <pubDate>Tue, 07 Jun 2022 14:41:33 +0000</pubDate>
      <link>https://forem.com/playtomic/modernizing-a-cicd-pipeline-with-github-actions-12gn</link>
      <guid>https://forem.com/playtomic/modernizing-a-cicd-pipeline-with-github-actions-12gn</guid>
      <description>&lt;p&gt;&lt;a href="https://www.sgmoratilla.com/2019-04-15-playtomic-pipeline" rel="noopener noreferrer"&gt;Our CI+CD has been working for 5 years long&lt;/a&gt;. You know, if it ain't broken, don't fix it. But the company is not the same. It's time to update it!&lt;/p&gt;

&lt;p&gt;Let me wrap up a bit our current setup. I am going to be brief, I promise. We have a Jenkins cluster on-premise. That is, we manage (and maintain) a bunch of hosts that run Jenkins slaves and a host that runs the master. They are within our on-premise VPN, a reminiscence of our first hosting provider.&lt;/p&gt;

&lt;p&gt;Our production backend runs a containerized system on top of a Docker Swarm cluster. Our container registry is Nexus, which allows us to deploy our services in the Swarm.&lt;/p&gt;

&lt;p&gt;Both systems run within their own independent VPNs.&lt;/p&gt;

&lt;p&gt;Our code repositories are in Github. Our Jenkins is listening for changes in Github. When we merge to develop/production, Jenkins pushes the image to the Nexus, connects via ssh to the managers of the cluster and runs the deployment command (&lt;em&gt;docker stack deploy&lt;/em&gt;). We have a common Jenkins pipeline for all our services.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh6qncjgrls3ebqtx2dll.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh6qncjgrls3ebqtx2dll.png" alt="CI+CD Pipeline with Jenkins" width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pros:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple.&lt;/li&gt;
&lt;li&gt;Security on our side (VPN certificates + ssh keys are in our servers).&lt;/li&gt;
&lt;li&gt;Jenkins is commonly known.&lt;/li&gt;
&lt;li&gt;Stable: we haven't had to change it a lot all this time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We need to maintain and keep update the Jenkins machines.&lt;/li&gt;
&lt;li&gt;As the team grows, the Jenkins cluster has to grow too to be able to run more jobs.&lt;/li&gt;
&lt;li&gt;We have only configured Java 8 and 11.&lt;/li&gt;
&lt;li&gt;Still on our old hosting provider.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We have been pretty happy with this setup so far. Maintenance is something that we always want to simplify at Playtomic. The fewer systems, the better. Besides, new versions of Java require new versions of the JDK, maven and thus... Jenkins. We have already been using Github Actions in other projects here, so that we know that it could be a fine replacement of Jenkins. The workflow defines the environment it is required to run (for example, the java version or the architecture), so it makes complete sense to use Github Actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replacing Jenkins by Github Actions
&lt;/h2&gt;

&lt;p&gt;We re-wrote the Jenkins pipeline as a Github Actions workflow. Our main concerns are two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What about security? We are using ssh-keys to access the cluster. We can use Github secrets to store them, but we don't like having such an important piece of the security stored in a third-party.&lt;/li&gt;
&lt;li&gt;Cost might be a problem in the future. Github Actions are charged by the minute of computation and we have 50 services.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We discovered that Github Actions allow you to run self-hosted nodes... so that's the solution to both problems. At this very moment, we can afford the minutes that we are spending, so we are adding a host just for deployments. We added a t3.nano, which is pretty cheap. ssh-keys are still 100% under our control, as they are installed in the machine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftcpkmkw1f2vttifuakw4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftcpkmkw1f2vttifuakw4.png" alt="CI+CD Pipeline with Github Actions" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pros:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Still a common pipeline (workflow) but allows everyone to use new stuff (new java version, different languages, ...) without installing more tools in Jenkins.&lt;/li&gt;
&lt;li&gt;Security is still on our side.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We need to monitor the cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Containerized Github Action runner
&lt;/h3&gt;

&lt;p&gt;Worried about the cost? If you have a cluster, &lt;a href="https://www.sgmoratilla.com/2022-06-07-docker-multiarch-github-actions-runner/" rel="noopener noreferrer"&gt;you can run as several copies as you want of the Github Action runner!&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  On-demand self-hosted runners
&lt;/h3&gt;

&lt;p&gt;This would be a huge improvement to control the cost while still being able to scale the number of runners: &lt;br&gt;
&lt;a href="https://github.com/machulav/ec2-github-runner" rel="noopener noreferrer"&gt;https://github.com/machulav/ec2-github-runner&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Separate the CI from the CD
&lt;/h3&gt;

&lt;p&gt;To separate pipeline for the CI and CD is a good idea, as their lifecycles are pretty different.&lt;br&gt;
In our current setup, the workflow is responsible for running the deployment command. If that fails, then the whole pipeline fails, but the build was successful. &lt;/p&gt;

&lt;p&gt;We have already tested ArgoCD in Kubernetes so that the CD is handled by the cluster itself.&lt;br&gt;
If you are running Kubernetes, you can already &lt;a href="https://dev.to/2021-10-28-flux-vs-argocd/"&gt;do that with FluxCD or ArgCD&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3sq4wqemb5gsatoyyw06.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3sq4wqemb5gsatoyyw06.png" alt="CI+CD Pipeline with Github Actions and ArgoCD" width="800" height="535"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Replace Nexus by Github Packages
&lt;/h3&gt;

&lt;p&gt;Nexus requires a lot of space (as it stores all the containers, libraries, packages, ... of your organization). We don't want to maintain that space, be responsible of the backups, ... so that we are considering to migrate to Github Packages too.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>githubactions</category>
      <category>jenkins</category>
    </item>
    <item>
      <title>MVI in Playtomic mobile app</title>
      <dc:creator>Angel G. Olloqui</dc:creator>
      <pubDate>Tue, 31 May 2022 12:06:15 +0000</pubDate>
      <link>https://forem.com/playtomic/mvi-at-playtomic-2pea</link>
      <guid>https://forem.com/playtomic/mvi-at-playtomic-2pea</guid>
      <description>&lt;p&gt;Last summer we finally decided to move out of our classic MVP+UIKit/Android view architecture into a more modern one with &lt;strong&gt;SwiftUI&lt;/strong&gt;/&lt;strong&gt;Compose&lt;/strong&gt; as main actors for our view layer. Together with the UI framework changes, we found the need of switching to a more reactive architecture that better fits the nature of declarative UIs.&lt;/p&gt;

&lt;p&gt;We spent some time analysing some of the most popular reactive architectures: MVVM, MVI and &lt;a href="https://github.com/pointfreeco/swift-composable-architecture"&gt;TCA&lt;/a&gt;. Without getting into much detail of our decision making (it would take a full post), we decided that &lt;strong&gt;MVI was the best fitting for our project&lt;/strong&gt;. With it, we could get better &lt;strong&gt;separation of concerns&lt;/strong&gt; and &lt;strong&gt;state management&lt;/strong&gt; than in MVVM, &lt;strong&gt;unidirectional data flow&lt;/strong&gt;, &lt;strong&gt;single source of truth&lt;/strong&gt; and &lt;strong&gt;easy of testing&lt;/strong&gt;, without the additional complexity added by TCA.&lt;/p&gt;

&lt;p&gt;After around half a year working with MVI, the &lt;strong&gt;team is highly satisfied&lt;/strong&gt;: all people in the team consider it a good/great choice and enjoys working with it, being the verbosity the only drawback.&lt;/p&gt;

&lt;p&gt;Let us share a bit on how we do it:&lt;/p&gt;

&lt;h2&gt;
  
  
  The state management layer
&lt;/h2&gt;

&lt;p&gt;This is how our MVI base class looks like in both platforms&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;open class BaseMVIPresenter&amp;lt;S: ViewState, A: ViewAction&amp;gt; {
    public var currentViewState: S { _viewState.value! }
    public var viewState: Observable&amp;lt;S&amp;gt; { _viewState }
    fileprivate let _viewState: MutableObservable&amp;lt;S&amp;gt;

    public init(initialState: S) {
        _viewState = MutableObservable(value: initialState)
    }

    func dispatch(action: A) {
        fatalError("Must be implemented by the children")
    }
}

open class MVIPresenter&amp;lt;S: ViewState, A: ViewAction, R: ActionResult&amp;gt;: BaseMVIPresenter&amp;lt;S, A&amp;gt; {
    private var middlewares: [MVIMiddleware&amp;lt;S, A, R&amp;gt;] = []

    public func with(middleware: MVIMiddleware&amp;lt;S, A, R&amp;gt;) -&amp;gt; Self {
        middlewares.append(middleware)
        return self
    }

    open func handle(action: A, results:  @escaping (R) -&amp;gt; Void) {
        fatalError("Must be implemented by the children")
    }

    open func reduce(currentViewState: S, result: R) -&amp;gt; S {
        fatalError("Must be implemented by the children")
    }

    override public func dispatch(action: A) {
        middlewares.forEach { element in
            element.handle(action: action, presenter: self)
        }
        handle(action: action) { [weak self] result in
            Executor.execute(inBackground: false) {
                guard let self = self else { return }
                self.middlewares.forEach { middleware in
                    middleware.handle(result: result, presenter: self)
                }
                self._viewState.value = self.reduce(currentViewState: self.viewState.value!, result: result)
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
interface BaseMVIPresenter&amp;lt;ViewState, ViewAction&amp;gt; {
    val currentViewState: ViewState get() = viewState.value!!
    val viewState: Observable&amp;lt;ViewState&amp;gt;
    fun dispatch(action: ViewAction)
}

abstract class MVIPresenter&amp;lt;S : ViewState, A : ViewAction, R : ActionResult&amp;gt;(initialState: S) : BaseMVIPresenter&amp;lt;S, A&amp;gt; {
    override val viewState: Observable&amp;lt;S&amp;gt;
        get() = _viewState
    private val _viewState = MutableObservable(value = initialState)
    internal var middlewares = mutableListOf&amp;lt;MVIMiddleware&amp;lt;S, A, R&amp;gt;&amp;gt;()

    abstract fun handle(action: A, results: (R) -&amp;gt; Unit)

    abstract fun reduce(currentViewState: S, result: R): S

    override fun dispatch(action: A) {
        middlewares.forEach { element -&amp;gt;
            element.handle(action = action, presenter = this)
        }
        handle(action) { result -&amp;gt;
            Executor.execute(inBackground = false) {
                this.middlewares.forEach { middleware -&amp;gt;
                    middleware.handle(result = result, presenter = this)
                }
                this._viewState.value = this.reduce(currentViewState = this.viewState.value!!, result = result)
            }
        }
    }
}

fun &amp;lt;S : ViewState, A : ViewAction, R : ActionResult, T : MVIPresenter&amp;lt;S, A, R&amp;gt;&amp;gt; T.with(middleware: MVIMiddleware&amp;lt;S, A, R&amp;gt;): T {
    middlewares.add(middleware)
    return this
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: In MVI there is no definition whether the data management part should be done in a presenter, view model or whatever. However, in our case we opted to call them "Presenters" to be more inlined with the rest of the app, but they are in practice maintaining state as classic ViewModel in Android.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then, when building a feature, we need to provide the implementation of 2 methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;handle&lt;/code&gt;: This method takes the actions triggered by some other component (normally the view) and &lt;strong&gt;handles the side effects&lt;/strong&gt;. It emits new events (called "action results") with the results of the effects, like for example a network call. It does not perform any state management or manipulation, it just emits new result events.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;reduce&lt;/code&gt;: Given a state and an action result, this method &lt;strong&gt;computes the next state of the view&lt;/strong&gt;. Note that it behaves as a pure function, with no side effects.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6wvqq48e--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3fjncta1jpx6x09j1g11.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6wvqq48e--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3fjncta1jpx6x09j1g11.png" alt="MVI diagram" width="523" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In contrast with other simpler implementations of MVI, we opted to split the code into these 2 methods to &lt;strong&gt;isolate side effects from state manipulation&lt;/strong&gt;, which make our tests much simpler and our overall solution more robust and clean. An added benefit is that there are &lt;strong&gt;no race conditions&lt;/strong&gt; possible like in other architectures, since all calls to &lt;code&gt;reduce&lt;/code&gt; are executed in serial with no partial updates possible.&lt;/p&gt;

&lt;p&gt;In addition, we added an extra piece around the presenters, called &lt;code&gt;Middleware&lt;/code&gt;, that are capable of &lt;strong&gt;reacting to events without doing state management&lt;/strong&gt;. For example, we can add all analytics tracking into a middleware or all navigation actions. This way, our presenter stays purist, just doing the state management part, and we have a set of small middlewares with a single other purpose, making it once again easier to test and maintain.&lt;/p&gt;

&lt;p&gt;Lastly, you can see how both platform implementations are quite similar, and they both avoid the usage of platform specific APIs like Combine or Flows (although they are used internally) to maximize code reusal when transpiling and also reduce the learning curve.&lt;/p&gt;

&lt;p&gt;An example Presenter would look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;internal class LessonDetailPresenter(...): MVIPresenter&amp;lt;LessonDetailState, LessonDetailAction, LessonDetailResult&amp;gt;(LessonDetailState.none) {

    override fun handle(action: LessonDetailAction, results: (LessonDetailResult) -&amp;gt; Unit) {
        when (action) {
            is LessonDetailAction.onAppear -&amp;gt; loadLesson(results, lessonId)            
            is LessonDetailAction.tapConfirmCancelEnrollment -&amp;gt; unregisterFromLesson(results)
            is LessonDetailAction.resendConfirmation -&amp;gt; resendConfirmation(results)
            ...
        }
    }

    override fun reduce(currentViewState: LessonDetailState, result: LessonDetailResult): LessonDetailState {
        return when (result) {
            is LessonDetailResult.lessonLoading -&amp;gt; LessonDetailState.loading
            is LessonDetailResult.lessonLoaded -&amp;gt; LessonDetailState.detail(sections = result.lesson.mapToLessonDetail(me = userId))
            ...
        }
    }

    private fun loadLesson(results: (LessonDetailResult) -&amp;gt; Unit, lesson: LessonId) {
        results(LessonDetailResult.lessonLoading)
        activityService.fetchLesson(lessonId)
            .then { results(LessonDetailResult.lessonLoaded(it)) }
            .catchError { results(LessonDetailResult.loadLessonByIdFailed(error)) }
        }
    }
    ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And some associated middleware for navigation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;internal class LessonDetailNavigator(...) : MVIMiddleware&amp;lt;LessonDetailState, LessonDetailAction, LessonDetailResult&amp;gt;() {

    override fun handle(action: LessonDetailAction, presenter: MVIPresenter&amp;lt;LessonDetailState, LessonDetailAction, LessonDetailResult&amp;gt;) {
        when (action) {
            LessonDetailAction.tapOpenMaps -&amp;gt; openMaps(presenter = presenter)
            LessonDetailAction.tapAddToCalendar -&amp;gt; addLessonToCalendar(presenter = presenter)
            ...
        }
    }

    override fun handle(result: LessonDetailResult, presenter: MVIPresenter&amp;lt;LessonDetailState, LessonDetailAction, LessonDetailResult&amp;gt;) {
        when (result) {
            is LessonDetailResult.loadLessonByIdFailed -&amp;gt; dismiss()
            else -&amp;gt; {}
        }
    }
    ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Our view layer
&lt;/h2&gt;

&lt;p&gt;Then, our views basically just receive 2 parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public struct LessonDetailView: View {
    @ObservedObject var state: ObservableViewState&amp;lt;LessonDetailState&amp;gt;
    let dispatcher: (LessonDetailAction) -&amp;gt; Void

    public var body: some View {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Composable
private fun LessonDetailView(
    viewState: LiveData&amp;lt;LessonDetailState&amp;gt;,
    dispatcher: (LessonDetailAction) -&amp;gt; Unit
) {
    ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, we are making use of &lt;a href="https://developer.android.com/jetpack/compose/state#state-hoisting"&gt;state hoisting&lt;/a&gt; for encapsulating the presenter injection (from the view layer, it does not know what class is behind the state management). This also makes our &lt;strong&gt;code much more reusable&lt;/strong&gt;, and very &lt;strong&gt;easy to setup the previews&lt;/strong&gt;, since we do not need to mock any data, service or presenter, just passing the view state down is enough. For this state hoisting we are making use of a parent &lt;code&gt;UIViewController&lt;/code&gt;/&lt;code&gt;Fragment&lt;/code&gt;, since our app is now a mixed app with only part of the views in SwiftUI/Compose. They look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;final class LessonDetailViewViewController: SwiftUIViewController&amp;lt;LessonDetailState, LessonDetailAction, LessonDetailView&amp;gt; {
    override func contentView(
        viewState: ObservableViewState&amp;lt;LessonDetailState&amp;gt;,
        dispatcher: @escaping (LessonDetailAction) -&amp;gt; Void
    ) -&amp;gt; LessonDetailView {
        LessonDetailView(state: viewState, dispatch: dispatcher)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class LessonDetailFragment : ComposeFragment&amp;lt;LessonDetailState, LessonDetailAction&amp;gt;() {
    @Composable
    override fun ContentView(
        viewState: LiveData&amp;lt;LessonDetailState&amp;gt;,
        dispatcher: (LessonDetailAction) -&amp;gt; Unit
    ) = LessonDetailView(viewState = viewState, dispatcher = dispatcher)    
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where the &lt;code&gt;SwiftUIViewController&lt;/code&gt; and &lt;code&gt;ComposeFragment&lt;/code&gt; are base classes that inject the presenter and create the &lt;code&gt;UIHostingController&lt;/code&gt;/&lt;code&gt;ComposeView&lt;/code&gt; for using SwiftUI/Compose inside with the content returned by the concrete &lt;code&gt;contentView&lt;/code&gt; method on each case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;There are tons of options and architectures to use with SwiftUI/Combine. In Playtomic, we opted for a MVI version where we have a clear separation of  the different responsibilities, single source of truth and a simple and unidirectional data flow. It also allows for very simple views and easy transpilation between platforms, with &lt;br&gt;
only one drawback so far: the extra boilerplate needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.godaddy.com/engineering/2021/11/05/android-state-management-mvi/"&gt;GoDaddy Studio’s Journey with State Management&lt;/a&gt;: Great article explaining some of the issues they found with MVP, MVVM and MVI in its simpler form. We find ourselves very aligned with their journey.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mvi</category>
      <category>architecture</category>
      <category>ios</category>
      <category>android</category>
    </item>
    <item>
      <title>Let's talk about performance and MongoDB</title>
      <dc:creator>Sergio Garcia Moratilla</dc:creator>
      <pubDate>Wed, 16 Feb 2022 10:21:38 +0000</pubDate>
      <link>https://forem.com/playtomic/lets-talk-about-performance-and-mongodb-4048</link>
      <guid>https://forem.com/playtomic/lets-talk-about-performance-and-mongodb-4048</guid>
      <description>&lt;p&gt;If you, a backend developer, had to describe your job, what would you say? We usually talk a lot about servers, clusters, layers, algorithms, software stacks, memory consumption, ...&lt;/p&gt;

&lt;p&gt;We put data into databases and we get it back as fast as we can.&lt;br&gt;
Databases are our cornerstone. Why don't we talk more often about them? We are always relying on our ORMs.&lt;/p&gt;

&lt;p&gt;My best advice? Simplify your queries. Simplify your data models. Simplify your access patterns.&lt;/p&gt;
&lt;h1&gt;
  
  
  Performance in MongoDB
&lt;/h1&gt;
&lt;h2&gt;
  
  
  Metrics
&lt;/h2&gt;

&lt;p&gt;If you are using MongoDB, these two metrics will be your best friends:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scanned Documents/Returned ratio&lt;/li&gt;
&lt;li&gt;IOPS &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Scanned docs/returned&lt;/strong&gt;: it means how many documents you are reading from disk vs the number of documents you are actually returning in your &lt;code&gt;find()&lt;/code&gt; or &lt;code&gt;aggregate()&lt;/code&gt;. Ideally: this should be 1 (every document read is returned). The only way to get it? All your queries must be covered by indexes. Indexes are in memory (or they are if they fit), so MongoDB doesn´t have to read and filter them from disk.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuu7j6uxfni68te822j71.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuu7j6uxfni68te822j71.png" alt="Example of Scanned documents/Returned ratio" width="800" height="342"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IOPS&lt;/strong&gt;: I/O operations per second. That is, operations on disk.  It is correlated with scanned docs, as they are read from disk. But there are more sources of IOPS, for example writes.&lt;br&gt;
Your disk will give you a limit to your maximum IOPS. Ours is 3000. &lt;/p&gt;

&lt;p&gt;Your goal is to keep IOPS below that threshold, and as low as possible. It is hard to know how many IOPS your query consumes, but it is easy to know the scanned docs/returned ratio. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ri7onm4eb585u0e10o3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ri7onm4eb585u0e10o3.png" alt="Example of IOPS" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Tools
&lt;/h2&gt;

&lt;p&gt;How can you analyze why your database is behaving as it is?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MongoDB Profiler&lt;/strong&gt;&lt;br&gt;
If you can afford to enable it, do it now. It's the best source of info. We use the Atlas MongoDB Profiler and it is worth every penny. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpq1a0hocgm95nk9z3h1s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpq1a0hocgm95nk9z3h1s.png" alt="Atlas MongoDB Profiler" width="800" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explain planner&lt;/strong&gt;&lt;br&gt;
The planner is the key to understanding your access patterns.&lt;br&gt;
There are several ways of calling it, but you can start with &lt;code&gt;explain()&lt;/code&gt; after your cursor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;db.our_collection.find(query).explain()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can use add &lt;code&gt;executionStats&lt;/code&gt; to get more data about the query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;db.our_collection.find(query).explain("executionStats")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://docs.mongodb.com/manual/reference/explain-results/" rel="noopener noreferrer"&gt;There are many stages&lt;/a&gt;, but these ones are the most important:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;COLLSCAN: the query is scanning the collection in disk. Pretty bad, as no index covered the search, so MongoDB has to read the whole collection. &lt;/li&gt;
&lt;li&gt;IXSCAN: the query is using an index to filter. It doesn´t mean that all the query is covered by the index, but at least some part.&lt;/li&gt;
&lt;li&gt;FETCH: the planner is reading the documents from the collection. If your query is returning documents, you will get a FETCH stage probably (&lt;a href="https://docs.mongodb.com/manual/indexes/#covered-queries" rel="noopener noreferrer"&gt;unless your query is covered by the index&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is an example of one of our queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;winningPlan: 
      { stage: 'COLLSCAN',
        filter: 
         { '$and': 
            [ { '$or': 
                 [ { 'invitaed_user_id': { '$eq': '1' } },
                   { owner_id: { '$eq': '1' } },
                   { 'player_id': { '$eq': '1' } } ] },
              { is_canceled: { '$eq': false } },
             ] } },
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not good, as it is scanning the whole collection.&lt;/p&gt;

&lt;p&gt;Another one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;db.our_collection.find(query).explain("executionStats")
executionStats: 
   { executionSuccess: true,
     nReturned: 22,
     executionTimeMillis: 0,
     totalKeysExamined: 24,
     totalDocsExamined: 22,
     executionStages: 
      { stage: 'FETCH',
        nReturned: 22,
        docsExamined: 22,
        inputStage: 
         { stage: 'OR',
           nReturned: 22,
           inputStages: 
            [ { stage: 'IXSCAN',
                nReturned: 0,
                indexName: 'example-index-1',
                indexBounds: 
                 { owner_id: [ '["1", "1"]' ],
                   start_date: [ '[MaxKey, MinKey]' ] } },
              { stage: 'IXSCAN',
                nReturned: 22,
                indexName: 'example-index-2'
                indexBounds: 
                 { 'player_id': [ '["1", "1"]' ],
                   start_date: [ '[MaxKey, MinKey]' ] ] },
                keysExamined: 23,
                dupsDropped: 0 }] } } },
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here you can two see IXSCANs, merged by and OR. After that, the query is fetching the documents. I am reading the query inside-out. &lt;code&gt;example-index-1&lt;/code&gt; is used to resolve one part of the query, and &lt;code&gt;example-index-2&lt;/code&gt; for the other part.&lt;/p&gt;

&lt;p&gt;Sometimes you will get a FETCH just after an IXSCAN: it means that the index covers only part of the filter. After that, the planner needs to read the documents from disk to finish the filter.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mongodb/mongo/blob/master/src/mongo/db/query/stage_types.h#L49" rel="noopener noreferrer"&gt;The complete list of stages? You need to check the code.&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Simplify your queries
&lt;/h1&gt;

&lt;h2&gt;
  
  
  $ors
&lt;/h2&gt;

&lt;p&gt;$ors are the devil. You, as a programmer, are used to thinking in $ors. You add a few $ors, your condition gets much more expressive. But guess what? Your query has gotten exponentially more complex. With every condition you add to the $or, you are adding one more combination of parameters.&lt;/p&gt;

&lt;p&gt;How does the planner resolve all those combinations? It needs an index for each of them. &lt;/p&gt;

&lt;p&gt;Do you remember the &lt;code&gt;explain()&lt;/code&gt; above? The query was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    $or: [{"owner_id: "1"}, {"player_id: "1"}].
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The indexes used?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example-index-1: {"owner_id: "1", "start_date": 1}
example-index-2: {"player_id: "1", "start_date": 1}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is another tip: bigger indexes can cover smaller queries as long as the fields are at the beginning of the index.&lt;br&gt;
We are not using start_date to filter.&lt;/p&gt;

&lt;p&gt;What would happen if I add an extra $or?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    $or: [{"owner_id: "1"}, {"player_id: "1"}].
    $or: [{"is_canceled": true}, "start_date": {$gt: ISODate("2022-02-02T00:00:00)}]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then there would need 4 combinations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{owner_id, is_canceled}, 
{owner_id, start_date}, 
{player_id, is_canceled], 
{player_id, start_date}, 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Denormalized fields
&lt;/h2&gt;

&lt;p&gt;If you find yourself filtering by several fields within an $or, or sorting by several fields, consider adding a denormalized field (based on the others).&lt;/p&gt;

&lt;p&gt;Yeah, my apologies to the &lt;a href="https://en.wikipedia.org/wiki/Third_normal_form" rel="noopener noreferrer"&gt;third normal form&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Keep reading, we will give an example pretty soon using the so-called &lt;code&gt;Summary Pattern&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  nulls are always the minimum value
&lt;/h2&gt;

&lt;p&gt;This is a minor trick, but still useful. Let's say you have a nullable field, and you have to filter (or sort descending) by that field.&lt;/p&gt;

&lt;p&gt;You will probably find yourself using a query similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
 $or: [
        field: {$exists: false}, 
        field: {$lte: value}
      ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is, if the field is null, then it passes the filter. Otherwise, compare. We used this filter to check if a player could join a match given their level and the level restrictions of the match.&lt;/p&gt;

&lt;p&gt;That's an $or. We don't like $ors. What would happen if we compare our value to null? Let's check the &lt;a href="https://docs.mongodb.com/manual/reference/operator/aggregation/sort/#ascending-descending-sort" rel="noopener noreferrer"&gt;sorting rules in MongoDB&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MinKey (internal type)
Null
Numbers (ints, longs, doubles, decimals)
Symbol, String
Object
...
MaxKey (internal type)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is, null is always the lower value when comparing (except for the MinKey object, we will talk about it later).&lt;/p&gt;

&lt;p&gt;Our query can be simplified by this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  field: {$lte: value}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works for $lt and $lte (lower than and lower or equal).&lt;br&gt;
It works too if you are sorting by descending order of &lt;code&gt;field&lt;/code&gt; &lt;code&gt;{field: -1}&lt;/code&gt; because the object with null will be at the end of the sort.&lt;/p&gt;
&lt;h2&gt;
  
  
  Counts are costly in MongoDB
&lt;/h2&gt;

&lt;p&gt;Counting seems an easy operation, but it is not. Even if you have an index, MongoDB needs to traverse the index due to the way &lt;a href="https://docs.mongodb.com/manual/indexes" rel="noopener noreferrer"&gt;MongoDB builds B-trees&lt;/a&gt;: they don't store the number of leaves that the sub-tree have. So they need to traverse the index until the end.&lt;/p&gt;

&lt;p&gt;Again, if you are counting using a query with $or, it makes the counting even more complex: the query needs to take into account possible repeated documents.&lt;/p&gt;

&lt;p&gt;For example, we used counts to compute the position of a player in a ranking (the original query was even more complex).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{ranking_id: ?0}, 
{
 $or: [
        { value: {$gt: ?1}},
    { value: {$eq: ?1}, last_modified: {$gt: ?2} }
 ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It required several indexes to count:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{ranking_id: 1, value: -1}
{ranking_id: 1, value: -1, last_modified: -1}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How can we avoid counting on several indexes? Add a single field that summarize the fields that you are filtering.&lt;/p&gt;

&lt;p&gt;For example: &lt;code&gt;weight = append(value, last_modified)&lt;/code&gt;&lt;br&gt;
With that new field, we only required one single index:&lt;br&gt;
Indexes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{ranking_id: 1, weight: -1}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is called the Summary Pattern.&lt;/p&gt;

&lt;h1&gt;
  
  
  How to build indexes
&lt;/h1&gt;

&lt;p&gt;Ok, indexes are our best tool to keep MongoDB as performant as possible. So the next step, how do we know what indexes we should build?&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance advisor
&lt;/h2&gt;

&lt;p&gt;If you are in Atlas, use the &lt;a href="https://docs.atlas.mongodb.com/performance-advisor/" rel="noopener noreferrer"&gt;Performance advisor&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;At some point, you will know your system better than the Performance Advisor, but it is a good starting point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clone your production collections and explain() it in local
&lt;/h2&gt;

&lt;p&gt;Test your indexes thoroughly before you put them in production: - it takes a lot of IOPS to build them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;once you built it, the planner is taking it into account too, even if it is not finally used.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Remember what we said before:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;COLLSCAN: bad.&lt;/li&gt;
&lt;li&gt;IXSCAN: good.&lt;/li&gt;
&lt;li&gt;FETCH: good if it is the final step. Bad in between.
Others that you will see:&lt;/li&gt;
&lt;li&gt;COUNT: ok.&lt;/li&gt;
&lt;li&gt;MERGE: ok-ish, you probably could do better.&lt;/li&gt;
&lt;li&gt;MERGE_COUNT: good.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the way, there are blocking and non-blocking stages, meaning that a stage needs to wait for the previous one before it can start computing results.&lt;/p&gt;

&lt;h2&gt;
  
  
  ESR: Equal-Sort-Range
&lt;/h2&gt;

&lt;p&gt;Have you ever wondered what fields should go first in an index? You need to follow this rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fields filtered by equals ($eq, $in in some cases, ...) go first.&lt;/li&gt;
&lt;li&gt;Then fields used in the sort stage. Remember that the index has to be built in the same order as you are sorting.&lt;/li&gt;
&lt;li&gt;Then fields filtered by a range ($lt, $lte, $gt, ...).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ESR is the most useful rule you will find to build indexes. You should read as much as you can about it until you understand it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.alexbevi.com/blog/2020/05/16/optimizing-mongodb-compound-indexes-the-equality-sort-range-esr-rule/" rel="noopener noreferrer"&gt;This post by Alex Belilacqua is a gem&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disambiguate equals
&lt;/h2&gt;

&lt;p&gt;If you have two fields that are going to be filtered by $eq, what should go first?&lt;/p&gt;

&lt;p&gt;The answer is that it doesn't matter. You don't need to worry about having a more balanced tree. &lt;/p&gt;

&lt;p&gt;Just keep in mind the ESR rule. If one of them goes in a &lt;code&gt;sort&lt;/code&gt; or a &lt;code&gt;range&lt;/code&gt;, then it goes the latter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rollover process
&lt;/h2&gt;

&lt;p&gt;Building an index is one of the most costly operations. Your IOPS will go nuts. If you need to do that in your production environment, and your collection is big enough, then we recommend you to use &lt;a href="https://docs.mongodb.com/manual/tutorial/build-indexes-on-replica-sets/" rel="noopener noreferrer"&gt;a rolling process&lt;/a&gt;. It starts the index build in a secondary, then promotes it to primary once the build is finished. You will be able to build any index even when your database load is high.&lt;/p&gt;

&lt;p&gt;In Atlas, it's just one click. &lt;/p&gt;

&lt;h2&gt;
  
  
  Remove / Hide indexes
&lt;/h2&gt;

&lt;p&gt;The more indexes you have, the worst for the planner. The planner runs the query through the indexes it has and then takes the most promising. &lt;/p&gt;

&lt;p&gt;Again, counting when you have several indexes is pretty bad. &lt;/p&gt;

&lt;p&gt;Sometimes, you cannot just remove an index in production. You can check using your profiler if it can be removed but you might not be 100%. One not-frequent query might launch a COLLSCAN and then you would miss that index.&lt;/p&gt;

&lt;p&gt;Luckily, since MongoDB 4.4 you can &lt;a href="https://docs.mongodb.com/manual/core/index-hidden/" rel="noopener noreferrer"&gt;hide indexes&lt;/a&gt;. We use them to detect what indexes we can remove safely. &lt;/p&gt;

&lt;h1&gt;
  
  
  Limit your queries
&lt;/h1&gt;

&lt;p&gt;Have you set a maximum limit of time to your queries? Why not? Do your clients have a request time out? Then it can be a healthy practice to avoid unexpected uses of your APIs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;db.our_collection.countDocuments(query, {maxTimeMS: 100})
MongoServerError: Error in $cursor stage :: caused by :: operation exceeded time limit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do you see all these orange dots? No one was waiting for the backend to reply.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc7pleurw87fvoogewkin.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc7pleurw87fvoogewkin.png" alt="maxTimeMS effect" width="800" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  References
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.mongodb.com/blog/post/building-with-patterns-a-summary" rel="noopener noreferrer"&gt;https://www.mongodb.com/blog/post/building-with-patterns-a-summary&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.alexbevi.com/blog/2020/05/16/optimizing-mongodb-compound-indexes-the-equality-sort-range-esr-rule/" rel="noopener noreferrer"&gt;https://www.alexbevi.com/blog/2020/05/16/optimizing-mongodb-compound-indexes-the-equality-sort-range-esr-rule/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mongodb</category>
    </item>
    <item>
      <title>Automating rollout releases in Android</title>
      <dc:creator>Angel G. Olloqui</dc:creator>
      <pubDate>Sat, 01 Jan 2022 10:14:06 +0000</pubDate>
      <link>https://forem.com/playtomic/automating-rollout-releases-in-android-1968</link>
      <guid>https://forem.com/playtomic/automating-rollout-releases-in-android-1968</guid>
      <description>&lt;p&gt;As part of our new release process (weekly releases) we are also &lt;strong&gt;changing the way we are publishing the apps for users&lt;/strong&gt;. Since it is now automatic, it is crucial for us to have a &lt;strong&gt;phased released&lt;/strong&gt; in which only a small subset of our users get the latest build, increasing daily and acting as a "failsafe" in case of an important bug makes it into production. &lt;/p&gt;

&lt;p&gt;For iOS, we can set the &lt;a href="https://help.apple.com/app-store-connect/#/dev3d65fcee1"&gt;Phased Release&lt;/a&gt; + Publication Date option and the AppStore will handle it in your behalf, starting the release on a certain date and increasing the automatic updates to 1%, 2%, 5%... each day.&lt;/p&gt;

&lt;p&gt;However, Google's approach is different. They do not offer a release date nor an automated phased release. Instead, they offer you with an &lt;a href="https://developers.google.com/android-publisher/api-ref/rest/v3/edits.tracks"&gt;API&lt;/a&gt; (and web dashboard) where you can set the percentage of users yourself at any time. This is in many senses much better than the Apple one, especially since this actually controls the releases and not just the automatic updates like in Apple, but it has a downside: &lt;strong&gt;it is all manual&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;With our current setup of weekly releases and a phased released across 6 days (2%, 5%, 10%, 20%, 50%, 100%), this basically means having to &lt;strong&gt;update every single day the rollout amount manually&lt;/strong&gt;. I am not sure about you, but having to enter every single day to click some button is the last thing I want to do.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i.giphy.com/media/ZWbeEcbeo0cKI/giphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://i.giphy.com/media/ZWbeEcbeo0cKI/giphy.gif" alt="Press the button" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating the release rollout
&lt;/h2&gt;

&lt;p&gt;So, we wondered, if we have an API to control the rollout percentage, isn't that enough to make it automatic? what if we have a CI job that run every day and basically checks if there is an ongoing rollout release, and in that case increases the percentage? Let's see how we did it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# rollout_update.py

import copy
import sys
import httplib2
from apiclient.discovery import build
from oauth2client.service_account import ServiceAccountCredentials
from oauth2client.client import AccessTokenRefreshError

TRACK = ('production')

# To run: rollout_update package_name json_credentials_path
def main():
  PACKAGE_NAME = sys.argv[1]
  credentials = ServiceAccountCredentials.from_json_keyfile_name(
    sys.argv[2],
    scopes='https://www.googleapis.com/auth/androidpublisher')

  http = httplib2.Http()
  http = credentials.authorize(http)

  service = build('androidpublisher', 'v3', http=http)

  try:
    edit_request = service.edits().insert(body={}, packageName=PACKAGE_NAME)
    result = edit_request.execute()
    edit_id = result['id']

    track_result = service.edits().tracks().get(editId=edit_id, packageName=PACKAGE_NAME, track=TRACK).execute()
    old_result = copy.deepcopy(track_result)

    print("Current status: ", track_result)
    for release in track_result['releases']:
        if 'userFraction' in release:
            rolloutPercentage = release['userFraction']
            if rolloutPercentage == 0:
                print('Release not rolled out yet')
                continue
            elif rolloutPercentage &amp;lt; 0.02:
                release['userFraction'] = 0.02                         
            elif rolloutPercentage &amp;lt; 0.05:
                release['userFraction'] = 0.05
            elif rolloutPercentage &amp;lt; 0.1:
                release['userFraction'] = 0.1
            elif rolloutPercentage &amp;lt; 0.2:
                release['userFraction'] = 0.2
            elif rolloutPercentage &amp;lt; 0.5:
                release['userFraction'] = 0.5
            elif rolloutPercentage &amp;lt; 1.0:
                del release['userFraction']
                release['status'] = 'completed'
            else:
                print('Release already fully rolled out')
                continue        
    if old_result != track_result:
        completed_releases = list(filter(lambda release: release['status'] == "completed", track_result['releases']))
        if len(completed_releases) == 2:
            track_result['releases'].remove(completed_releases[1])

        print("Updating status: ", track_result)
        service.edits().tracks().update(
                    editId=edit_id,
                    track=TRACK,
                    packageName=PACKAGE_NAME,
                    body=track_result).execute()
        commit_request = service.edits().commit(editId=edit_id, packageName=PACKAGE_NAME).execute()
        print('Edit ', commit_request['id'], ' has been committed')    


  except AccessTokenRefreshError:
      raise SystemExit('The credentials have been revoked or expired, please re-run the application to re-authorize')

if __name__ == '__main__':
  main()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order to run this step, you need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get a Google Developer Service JSON key credentials file. If you have being using some automation tools like Fastlane for uploading the APK to the GooglePlay, you have this already. Otherwise, follow the instructions from &lt;a href="https://docs.fastlane.tools/actions/supply/"&gt;Fastlane Supply setup&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Install &lt;code&gt;pipenv&lt;/code&gt; or some dependency manager for python since the script uses &lt;code&gt;google-api-python-client&lt;/code&gt; and &lt;code&gt;oauth2client&lt;/code&gt;. You could get them by running:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pipenv install google-api-python-client                            
pipenv install oauth2client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Run the script:
&lt;code&gt;pipenv run rollout_update.py &amp;lt;your_package&amp;gt; &amp;lt;json_credential_path&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The script will do the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new &lt;a href="https://developers.google.com/android-publisher/edits"&gt;Google Edit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Fetch the production track release info&lt;/li&gt;
&lt;li&gt;For each release, if it has a rollout in progress, then it increases the rollout percentage to the next "step", where steps are: 2%, 5%, 10%, 20%, 50%, 100%.&lt;/li&gt;
&lt;li&gt;If changes performed, then commit the Edit&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Connecting the CI
&lt;/h2&gt;

&lt;p&gt;So now that we have the script to increase the rollout, all we need is to schedule it. In our case, we are using &lt;a href="https://www.bitrise.io/"&gt;Bitrise&lt;/a&gt;, so we decided to schedule a workflow that runs the script every night. We even created a &lt;a href="https://github.com/angelolloqui/bitrise-step-google-play-rollout-update"&gt;Bitrise step&lt;/a&gt; in case you want to use it that handles the dependencies and running the script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  update_rollout:
    steps:
    - git::https://github.com/angelolloqui/bitrise-step-google-play-rollout-update.git@master:
        inputs:
        - package_name: com.playtomic
        - service_account_json_key_path: "$BITRISEIO_BITRISEIO_GOOGLEPLAY_SERVICE_ACCOUNT_JSON_URL"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: Your credentials file should be stored somewhere secured, like the Generic File Sorage of Bitrise&lt;/p&gt;

&lt;h2&gt;
  
  
  Halting a failing release?
&lt;/h2&gt;

&lt;p&gt;If a release goes wrong, all you need to do is to go to the Google Dashboard and halt the release as you would normally do with a manually controlled phased release. The script will simply detect the release is halted and will just ignore it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about Hotfixes?
&lt;/h2&gt;

&lt;p&gt;Well, the script makes no assumption on the type of build or release, so it will basically just work for any release, including hofixes. However, normally in hotfix builds, since they tend to be critical (otherwise we would not make a hotfix but wait for next week build) we normally deploy it to 100% of the user base, so we do not really need this step to run.&lt;/p&gt;

</description>
      <category>android</category>
      <category>googleplay</category>
      <category>automation</category>
      <category>bitrise</category>
    </item>
    <item>
      <title>Decorator pattern vs inheriting in Kotlin</title>
      <dc:creator>Manuel González Villegas</dc:creator>
      <pubDate>Mon, 13 Dec 2021 08:15:03 +0000</pubDate>
      <link>https://forem.com/playtomic/decorator-pattern-vs-inheriting-in-kotlin-1ihh</link>
      <guid>https://forem.com/playtomic/decorator-pattern-vs-inheriting-in-kotlin-1ihh</guid>
      <description>&lt;p&gt;In playtomic, especially in the mobile team, we are fan of the decorator pattern, and in this article I will try to explain why it is a better idea to use the decorator pattern than inheritance, showing an example of how we use it.&lt;/p&gt;

&lt;p&gt;This article will be technical, so I recommend to you to read before a bit of theory about the difference between inheritance and the decorator pattern. &lt;a href="https://newbedev.com/decorator-pattern-versus-sub-classing"&gt;https://newbedev.com/decorator-pattern-versus-sub-classing&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  The inheritance
&lt;/h4&gt;

&lt;p&gt;The inheritance in Kotlin is the same as in some other languages, we use the inheritance when we know there is a relationship between a child and its parent class, for example, an iPhone is a phone, a phone is an electronic device. Each child is an specialised version of the parent in which inherits all the properties of the parent.&lt;/p&gt;

&lt;h4&gt;
  
  
  The decorator pattern
&lt;/h4&gt;

&lt;p&gt;The decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class.&lt;br&gt;
The coolest of the decorator pattern is that you can create a decorator class and use it more than once, and at any moment you can add or remove any other one (with the inheritance you can not).&lt;/p&gt;
&lt;h4&gt;
  
  
  An example in playtomic
&lt;/h4&gt;

&lt;p&gt;Let’s see an example about how Playtomic app manages the HTTP requests. As a spoiler I will tell you that with the decorator pattern we can change which http client we are using in runtime, yes, in runtime. Let 's see.&lt;/p&gt;

&lt;p&gt;To start, we have a &lt;code&gt;IHttpClient&lt;/code&gt; interface, which is a basic thing that we will need to create the decorator classes. In this case we only have one simple method to make a request with an object that contains the needed information to make that request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interface IHttpClient {
    fun request(httpRequest: HttpRequest): Promise&amp;lt;HttpResponse&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our case, we also have some other implemented methods (inside the interface) to make our lives easier while we are making requests. All of them are very similar to this one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    fun get(endpoint: String, params: Map&amp;lt;String, Any&amp;gt;?): Promise&amp;lt;ByteArray&amp;gt; =
            request(HttpRequest(method = HttpMethod.get, url = endpoint, queryParams = params))
                    .then(map = { it.body })
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Well, I am not going to show very deeply the implementation of each of our http clients implementation, I will just enumerate some of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OkHttpClient =&amp;gt; This one is the main client that makes the remote http requests to our backend
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class OkHttpClient(
        private val baseUrl: String,
        timeOut: Long? = null,
        client: OkHttpClient? = null,
        private val urlEncoder: IHttpParameterEncoder = HttpUrlParameterEncoder(),
        private val bodyEncoders: List&amp;lt;IHttpParameterEncoder&amp;gt; = listOf(HttpJsonParameterEncoder(), ...))
    : IHttpClient {
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the base HTTP client, so here there is nothing too much to see.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AuthHttpClient =&amp;gt; With this one, we include the authentication and if the session has expired we can re-login to the user and throw the request again (and for the user is totally transparent)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class AuthHttpClient(private val httpClient: IHttpClient,
                     private val keychain: IKeyValueStorage) : IHttpClient by httpClient {
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case we are decorating the http client to add the authentication into the request, so, as you can see into the class definition (&lt;code&gt;... by httpClient&lt;/code&gt;) means that the interface that this class is implementing &lt;code&gt;IHttpClient&lt;/code&gt; interface is being decorated by the parameter &lt;code&gt;httpclient&lt;/code&gt;. So, in that case we can only implement into this class the methods that we need for the decoration (In this case, is the only one that we have). So, it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    override fun request(httpRequest: HttpRequest): Promise&amp;lt;HttpResponse&amp;gt; =
            request(httpRequest, refreshTokenAllowed = true)

    fun request(httpRequest: HttpRequest, refreshTokenAllowed: Boolean): Promise&amp;lt;HttpResponse&amp;gt; {
        val headers = httpRequest.headers?.toMutableMap() ?: mutableMapOf()
        val accessToken = keychain.accessToken
        if (accessToken != null &amp;amp;&amp;amp; isAnemoneRequest(httpRequest)) {
            headers.put("Authorization", "Bearer $accessToken")
        }
        val authHttpRequest = httpRequest.copy(headers = headers)
        val pendingPromise = PendingPromise&amp;lt;HttpResponse&amp;gt;()
        httpClient.request(authHttpRequest).then(pendingPromise::fulfill).catchError { error -&amp;gt;
            if (this.isAnemoneRequest(httpRequest) &amp;amp;&amp;amp; this.isSessionError(error)) {
                if (refreshTokenAllowed) {
                    if (accessToken != this.keychain.accessToken) {
                        this.request(httpRequest = httpRequest, refreshTokenAllowed = false).then(pendingPromise::fulfill).catchError(pendingPromise::reject)
                    } else {
                        this.refreshToken(PendingRequest(request = httpRequest, promise = pendingPromise, error = error))
                    }
                } else {
                    this.logout?.invoke()
                    this.httpClient.request(httpRequest).then(pendingPromise::fulfill).catchError(pendingPromise::reject)
                }
            } else {
                pendingPromise.reject(error)
            }
        }
        return pendingPromise
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As I said before, we are including into the request a header with the authentication needed information and checking if the request fails because of a session expired error to throw the request again.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AnalyticsHttpClient =&amp;gt; This one measures and track the response, so with this we can analyse the traffic, the error rate ...
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class AnalyticsHttpClient(private val httpClient: IHttpClient,
                          private val analyticsManager: IAnalyticsManager) : IHttpClient by httpClient {
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This decorator is doing something very similar to the previous one to be able to track timings and errors&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LocalHttpClient =&amp;gt; This one use as response some jsons stored into the app
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class LocalHttpClient(private val context: Context) : IHttpClient {
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is another base client to read local documents in JSON to be able to serve it as a response.&lt;/p&gt;

&lt;p&gt;And if you are still asking how we are changing the used http client in runtime, I will confess that it has nothing to do with the decorator pattern but this is how we are doing it. We have another implementation of the &lt;code&gt;IHttpClient&lt;/code&gt; interface that receives 2 implementations of the HTTP client and depending on the endpoint url and method verb we decide which one to use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class AppHttpClient(val appVersion: String,
                    var localHttpClient: IHttpClient,
                    var remoteHttpClient: IHttpClient)
    : IHttpClient {

    private val localEndpoints = mapOf&amp;lt;String, String&amp;gt;(
            "POST /v2/auth/login" to "/v2/auth/login",
            "GET /v2/users/me" to "/v2/users/me",
            "GET /v2/tournaments/7c0b8e6f-3693-4f48-b370-b9643a8d04fe" to "/v2/tournaments/1",
            "GET /v2/status/version_control" to "/v2/status/version_control",
            "" to ""
    )

    override fun request(httpRequest: HttpRequest): Promise&amp;lt;HttpResponse&amp;gt; {
        val headers = httpRequest.headers?.toMutableMap() ?: mutableMapOf()
        headers.put("X-Requested-With", "${BuildConfig.APPLICATION_ID}.app $appVersion")
        headers.put("User-Agent", "Android ${android.os.Build.VERSION.RELEASE}")

        val appHttpRequest = httpRequest.copy(headers = headers)
        val key = "${appHttpRequest.method.description} ${appHttpRequest.url}"

        val file = localEndpoints[key]
        return if (file != null) {
            localHttpClient.request(appHttpRequest.copy(url = file))
        } else {
            remoteHttpClient.request(appHttpRequest)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To complete this post and understand the power of the composition thanks to the decorator pattern, let’s take a quick look at how easy it is to add a new decorator component, to include a new behavior or remove one of it just by adding or removing the proper line into the instantiation. Ours one looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        val baseHttpClient = OkHttpClient(baseUrl = baseUrl, timeOut = 30)
        val authHttpClient = AuthHttpClient(
                httpClient = baseHttpClient,
                keychain = keychain
        )
        val analyticsHttpClient = AnalyticsHttpClient(
                httpClient = authHttpClient,
                analyticsManager = managerProvider.analyticsManager
        )
        val appHttpClient = AppHttpClient(
                appVersion = appVersion,
                localHttpClient = LocalHttpClient(applicationContext),
                remoteHttpClient = analyticsHttpClient
        )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
    </item>
    <item>
      <title>Neo4j 101</title>
      <dc:creator>Nacho González Bullón</dc:creator>
      <pubDate>Wed, 10 Nov 2021 17:03:35 +0000</pubDate>
      <link>https://forem.com/playtomic/neo4j-101-52ee</link>
      <guid>https://forem.com/playtomic/neo4j-101-52ee</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In Playtomic we have added a new database to the family, Neo4j. This is the first post of a series where we will try to explain what it is, how we use it and more interesting things about this database.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Neo4j
&lt;/h2&gt;

&lt;p&gt;We have plenty of options when picking a database for our project. The range usually goes from classic relational databases (MySql or PostgreSQL) to NoSQL databases. In the latter group, we often use document-oriented databases (MongoDB, CouchDB), wide-column stores (BigTable, Cassandra), or key-value storage (Redis, Dynamo). But sometimes the data we need to store and query does not fit well in any of these types. For some of these cases, we can use Neo4j.&lt;/p&gt;

&lt;p&gt;Neo4j is a graph database, this means that data is organised in nodes and relationships. A node fits into the entity in any other database, it has one or more labels and any number of properties. A relationship relates two given nodes, it also has a label, a direction, and any number of properties. So graph databases have entities and relationships, nothing new. But what makes them shine and be different from the rest of the databases is that they treat relationships as first-class citizens, as the entities. So, Neo4j is a perfect suit when you need to know about the relationship between your data. This commonly includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fraud detection&lt;/strong&gt;: by detecting uncommon patterns of relationships between nodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real time recommendations&lt;/strong&gt;: based on the relationships between the nodes we can provide useful recommendations to the users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network representations&lt;/strong&gt;: this will allow the network managers to analyse and predict problems and design better topographies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity and access management&lt;/strong&gt;: for complex cases, a database capable of traversing relationships within milliseconds might be the way to go.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But there is more, Neo4j comes with many graph algorithms that will allow us to analyse and take the best out of our data. They are organised in six big groups:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Community detection&lt;/strong&gt;: to evaluate how the nodes are forming communities, whether there are partitions or detect weak links in the graph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Centrality&lt;/strong&gt;: to determine the importance of distinct nodes in the graph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Similarity&lt;/strong&gt;: to evaluate how alike nodes are.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heuristic link prediction&lt;/strong&gt;: to predict new relationships based on the topology of the network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pathfinding &amp;amp; search&lt;/strong&gt;: two find shortest paths between nodes or find if there are available paths between nodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node embedding&lt;/strong&gt;: to compute low-dimensional representation of the graph to be used for machine learning.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to install and start working with Neo4j
&lt;/h2&gt;

&lt;p&gt;The easiest way to install Neo4j is &lt;a href="https://neo4j.com/download-neo4j-now"&gt;Neo4j Desktop&lt;/a&gt;. This app will allow us to create local databases, install tools and plug-ins and start working with our data. It also allows us to connect to our remote databases (cloud provided or Docker instances, for example). The app will also provide several features such as: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;database information,&lt;/li&gt;
&lt;li&gt;access to commands to load example datasets or different tutorials and exercises&lt;/li&gt;
&lt;li&gt;storage of your favourite queries&lt;/li&gt;
&lt;li&gt;keep a history of your queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And of course you can run your &lt;strong&gt;CYPHER&lt;/strong&gt; queries taking advantage of the syntax highlighting and getting your results in several formats such as table or graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cypher: what is it and first queries
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: to run the following examples you can run &lt;code&gt;:play northwind-graph&lt;/code&gt; with Neo4j Desktop in your local database and you will be presented with some instructions to load the sample data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Cypher is the query language used in Neo4j. It shines for its pattern matching syntax that produces very readable queries. This pattern matching syntax consists in &lt;em&gt;drawing&lt;/em&gt; the path you want your query to traverse. In the patterns we can determine nodes (enclosed in parenthesis), relationships (enclosed in square brackets) and the direction of the relationship using ASCII characters (-&amp;gt; or &amp;lt;-). The simplest query we can think of is the one that returns all the nodes of a given type with a limited number of results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;p:&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we say that we want to &lt;em&gt;match&lt;/em&gt; all nodes of type &lt;em&gt;Product&lt;/em&gt; and return them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--R6T7s2Wq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/970xxel9ygbs8k7v8vjq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--R6T7s2Wq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/970xxel9ygbs8k7v8vjq.png" alt="Results from MATCH(p:Product) RETURN p LIMIT 25" width="880" height="713"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now we can think of a more complex query where we can retrieve products and the category or categories they belong to.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;p:&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:PART_OF&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;c:&lt;/span&gt;&lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we want to &lt;em&gt;match&lt;/em&gt; all nodes of type &lt;em&gt;Product&lt;/em&gt; and the &lt;em&gt;Category&lt;/em&gt; they are &lt;em&gt;PART_OF&lt;/em&gt; and we define the direction of the relationship. Finally we limit the results to 25.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--PFz57DRK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hnfbodw1aryq6fck4k36.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PFz57DRK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hnfbodw1aryq6fck4k36.png" alt="Results from MATCH (p:Product)-[:PART_OF]-&amp;gt;(c:Category) RETURN p, c LIMIT 25" width="880" height="718"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But of course the Cypher language allows us to filter the results. We can, for example, get the orders of a given product.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;p:&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;productName:&lt;/span&gt;&lt;span class="s1"&gt;'Mozzarella di Giovanni'&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:ORDERS&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;o:&lt;/span&gt;&lt;span class="k"&gt;Order&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we are filtering products by its &lt;em&gt;productName&lt;/em&gt;. Also pay attention to the direction of the arrows as now they go from right to left, because and &lt;em&gt;order orders products&lt;/em&gt;. Of course you can write the query inverting the order of the nodes along with the direction of the arrows and in both cases the result would be exactly the same.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;o:&lt;/span&gt;&lt;span class="k"&gt;Order&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:ORDERS&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;p:&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;productName:&lt;/span&gt;&lt;span class="s1"&gt;'Mozzarella di Giovanni'&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt; 
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Nj0_z1TJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a7ktqlvakeuaez07h522.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Nj0_z1TJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a7ktqlvakeuaez07h522.png" alt="Results from MATCH (p:Product {productName:'Mozzarella di Giovanni'})&amp;lt;-[:ORDERS]-(o:Order) RETURN p, o LIMIT 25" width="880" height="718"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But we can go even further and query the database to get all the orders that contain &lt;em&gt;Mozzarella di Giovanni&lt;/em&gt; and the order consists of more than 4 products.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;o:&lt;/span&gt;&lt;span class="k"&gt;Order&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:ORDERS&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;p:&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;productName:&lt;/span&gt;&lt;span class="s1"&gt;'Mozzarella di Giovanni'&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt; 
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;apoc.node.degree&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'ORDERS&amp;gt;'&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;numOfProducts&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;numOfProducts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;numOfProducts&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we are taking advantage of two new concepts: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;em&gt;WITH&lt;/em&gt; clause that allows us to pipe query parts from one part of the query to the next. In this case we are using it to count the number of products in an order to filter them. &lt;/li&gt;
&lt;li&gt;the &lt;em&gt;apoc&lt;/em&gt; library that provides us with tons of procedures and functions to perform different tasks. In this case we are using the &lt;em&gt;apoc.node.degree&lt;/em&gt; function to get the number of &lt;em&gt;ORDERS&lt;/em&gt; relationships that come out of each &lt;em&gt;Order&lt;/em&gt; node.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LiIlr4GO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zh883tbuua2przrn5cym.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LiIlr4GO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zh883tbuua2przrn5cym.png" alt="Results from MATCH(o:Order)-[:ORDERS]-&amp;gt;(p:Product{productName:'Mozzarella di Giovanni'}) WITH apoc.node.degree(o,'ORDERS&amp;gt;') AS numOfProducts, p, o&amp;lt;br&amp;gt;
WHERE numOfProducts &amp;gt; 4 RETURN p, o, numOfProducts LIMIT 25" width="880" height="713"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These are the basics of &lt;strong&gt;Cypher&lt;/strong&gt;, obviously there are much more of it and you can continue learning in their &lt;a href="https://neo4j.com/developer/cypher/"&gt;guides and documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;Neo4j is an interesting player in the databases market and it is worth a try if your project needs what it provides. We will add more posts to these series to explain some other interesting aspects of Neo4j.&lt;/p&gt;

&lt;p&gt;Cover photo by &lt;a href="https://unsplash.com/@clintadair?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText"&gt;Clint Adair&lt;/a&gt; on &lt;a href="https://unsplash.com/s/photos/connections?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>neo4j</category>
      <category>cypher</category>
      <category>graph</category>
    </item>
  </channel>
</rss>
