Dive into the latest industry wisdom from

our experts


The Not So Equivalent Code: Demystifying Async Publisher
Working in agency development provides the benefit of frequently starting new projects, allowing us to start fresh - with a clean slate.
It means we can take advantage of new APIs provided by Apple without worrying about supporting older iOS versions with legacy code. We can also build upon what worked well in previous projects while eliminating any previous shortcomings.
For instance, in our most recent projects, we incorporated Swift's modern concurrency, which significantly streamlined the code in our application. It resulted in easy-to-follow code and reduced the likelihood of errors compared to similar projects that used Combine.
Using Modern Concurrency and one thoughtful comment in a code review from my colleague helped me realize a lot about the obvious - a behavior of AsyncSequence1, as well as the safety issues AsyncPublisher poses.
It all started with a test
Code written with async await. The next step was to write a test for its functionality. Since the test involved observing a @Published property in a SignInViewModel, my initial instinct was to utilize Combine. One of the resulting tests took the following form. Implementation details of SignInViewModel are beyond the scope of this article. Its primary function involves validating the inputs (email and password) when certain conditions are met. In this instance, the error is shown under the text field when it stops being the first responder.
The test passed, and I was happy and submitted it as a part of a merge request with other sign-in functionality and tests.
During the code review, a colleague suggested using async-await in the test instead of Combine. We briefly discussed it, and both believed the result would be equivalent. The idea led me down a rabbit hole that took me some time to navigate. The rewritten tests using Swift’s for await loop were failing randomly. It made me question the validity of my code and the implementation details of the SignInViewModel. When I looked into the details, the failure was caused by a timeout – there was a different amount of received values than expected. As I debugged the code, I discovered that observing viewError in the for-await loop resulted in various counts of the received values. I discovered that the behavior of AsyncSequence was different from what I had expected, despite reading various books, articles, and documentation.
Let’s put rubber boots on and start digging.
The first dig: Comparing AsyncPublisher vs subscribing via Combine
I will leave SignInViewModel for the rest of this article. Let us start with something simple and continuously increase the complexity to learn what is happening here; why the tests were failing and why they did so randomly. Let us leave the documentation behind, observe their behavior and devise the reasoning once we get there.
Let us first start with preparing a publisher. PasshthroughSubject should be a good candidate for this:
And let us observe it with basic sink in Combine and via for await loop, and print the received value.
There is no observable content, so let us send something through the pipe:
Now, if you look into the console, you will see lots of printed results– and if you look deep into the prints, most probably will be the same amount of prints from both sources – AsyncPublisher being the same as a basic sink.
So, why was it failing the test and not here? I got the notion of what is happening based on the initializer on AsyncStream. The initializer looks like this:
What caught my attention is the bufferingPolicy parameter, which decides how to buffer unused sequence items – whether they should stay there (and be buffered) or go. Let us test this assumption that the difference of for-await loop and sink would be noticeable when we introduce some delay with some heavy workload – such as finding the nth prime number in form of calling primeCount(100)2.
After introducing the delay, we finally have a minimum observable example of the issue. As you can see, the number of prints no longer corresponds - some numbers in the for-await loop are skipped, resulting in varying results across runs. It's worth noting that the highest number in the async for loop often ends in numbers other than 99, such as 97, which is the last number processed correctly.
Why does the for-await loop behave like this? It likely uses a buffering policy that discards some elements that aren't immediately consumed. While the exact buffering policy setting isn't crucial to this investigation, as you will see in a section about AsyncStream, it buffers a lot.
However, what's most important is how the for-await loop works. It only continues the iteration after the closure of the current iteration is over, ensuring that the for loop iterates synchronously while getting the items in a sequence asynchronously.
Counting differences
While it is interesting to see the number of prints varies, it still requires us to look at the code and try to guesswork. Let us prepare a counter to have a more reliable representation of results. I made the counter using an actor that takes a type as a parameter so that we can have a clear console:
The implementation is as follows:
As you can see, after every call to addToCount() is made, the counter increases by one and the count of the type is printed out, which means that the last print of a given type will be the relevant one for us.
Let us test it and see the results, first by instantiating the counters and using them inside our existing code (in Combine fashion, we have to create a Task to call asynchronous function):
Now we can finally see in numbers how much has been lost. The Combine counter’s last print is 100. For async, it is 83. Therefore, 17 % have been lost on my M1 Max without optimizations.
Making for await loop of AsyncPublisher work
We now have established a baseline and a test environment. We have observed that Combine works without any issues. Now let's focus on fixing the for-await loop to obtain the desired results.
Fortunately, the fix is straightforward. We need to create a Task and handle the for-await loop asynchronously without waiting. Doing so will cause us to lose the benefits of sequential code execution that Modern Concurrency provides. However, it will fix the issue that we have been dealing with.
This solution works because creating a Task is not a computationally expensive action that would cause us to lose an integer. Under normal circumstances, this approach should work well.
AsyncPublisher is not safe
In the previous section, we got to a point in which everything worked the way we desired – by asynchronously processing the items in the sequence, we were getting the same results from observing the for-await loop as with the Combine. Does this mean we can safely use AsyncPublisher without worrying about losing inputs? Unfortunately, no.
All it takes is to send a value concurrently, and we will be back where we started. For instance, in my working environment, after the following code was executed, the counter in Combine was at 1000, on async with 695. Not great.
Observing with a for-await loop is unfortunately not secure and can lead to data loss. Depending on the situation, there may be some ways to mitigate this issue. For example, if we know how many items we will receive, we can use collect() on the publisher. However, I do not see any good way to make the for-await loop work in the same way as subscribing to the publisher in Combine, where we can always handle it through async code. I have tried several other options, such as creating an AsyncIterator out of AsyncPublisher, but that did not yield any positive results.
These limitations are inherent to the for-await loop, as it only works on one thread and does not make any guarantees. However, we can leverage this limitation to our advantage in certain use cases, particularly when using it with AsyncSequence - or its implementation, AsyncStream.
AsyncStream to the rescue
AsyncStream is a built-in implementation of AsyncSequence in an asynchronous context. It allows awaiting its results in a sequence as well as allows publishing it (yield in async terminology) to sequence at any time with its continuation.
It is very easy to instantiate it, for our purposes, we can do something like this:
As you can see, I am storing the asyncStream in a variable for us to use later in the code, as well as storing the continuation. Although we unsafely unwrap the variables, the code is safe because AsyncStream returns the continuation in its init immediately (the provided closure does not escape the current scope).
Observing the AsyncStream is simple – we just need to make a simple change in our for-await loop. Since AsyncStream is an AsyncSequence, we can iterate it directly, just like any other Collection (such as Array). The only difference is that we do it asynchronously.
Continuing with our example, here's how we would yield the integers to the continuation:
With AsyncStream, the counter gets to 100 every time. The same applies when we count to 1000 with the code we already used. Not an integer gets wasted.
We no longer have to even process the items asynchronously, so we can simplify the observing code to just:
And why is it that AsyncStream works? It is because of its buffering policy - which is defined in its initialization. By default, the policy is unbounded, meaning every integer gets buffered regardless of how long it takes to process a single iteration.
We could very well destroy the ability of our AsyncStream if we wanted. For example, if we initialized AsyncStream with bufferingPolicy: .bufferingNewest(1), the counter would show only 2 instead of 1000. It supports the hypothesis that AsyncPublisher has a buffering policy, possibly a substantial one, but we still need to understand its limits for situations where we need it.
AsyncStream is a hero in our story because it allows us to set the buffering policy to fit our needs and expectations. It enables us to observe asynchronous values in an asynchronous context.
But what if we needed to observe a Combine publisher?
The question is: How do we asynchronously observe a Combine publisher? As I showed you earlier, we can achieve this by simply calling Task {} in the sink of the subscriber. However, this method doesn't allow us to use the ordered processing of the items that a for-await loop does.
If we create our own publisher, my recommendation is to use AsyncStream instead of Combine publishers, as long as we don't need to use reactive magic for other parts of the code.
If we cannot create our own AsyncStream or we want to observe a publisher we did not create, we can always create an AsyncStream, subscribe to the publisher, and yield the values to the AsyncStream.
And – to speak less abstractly – I think this may be a nice way to signify the differences between observing a stream in a for-await loop and awaiting the results and observing in Combine with sink in showing the information messages in the application.
Let’s say we have a following function from which we show the infoBar:
If we observe the publisher in Combine, we get the following result. As you can see, the messages get shown over each other - not the greatest behavior.

But we can always do it from AsyncStream and await the result of the completion via withCheckedContinuation, like this:
And the result is a readable message that is shown one after the other. We get the behavior we want without any difficulties. And what is best, we can use Modern Concurrency, so the code is quite readable.

Final Thoughts
Incomplete documentation is the cause of our limited understanding of Swift's Modern Concurrency, leaving us with much still to learn. As a result, we can only presume and guess. While we could learn more by analyzing the source code, doing so may not be a good use of our time, and it may be difficult to grasp all the details just by looking at it.
Therefore, further investigation in this area is needed. I hope more people will delve deeper instead of only scratching the surface.
In the meantime, let's not rush into rewriting every bit of code in async-await without careful consideration. We should always check our assumptions to avoid introducing unnecessary bugs.
1 In essence, AsyncPublisher enables observing any result from a publisher in an async for–await loop. You can find more in documentation https://developer.apple.com/documentation/combine/asyncpublisher
2 You can find a gist here: https://gist.github.com/lvalenta/07770221e7d653f6cfad6f2bda63af73 – It is just a random function a colleague found

Cleevio's Take on Creating Meaningful Client Relationships
Building a successful business partnership with your clients is essential in today’s competitive business environment. We believe it is necessary to create stable and lasting relationships with our clients that go beyond just sales. Here are some essentials we see as crucial in building business partnerships with our clients that go far beyond sales.
Listen and Understand
Listening to our clients and understanding their needs is fundamental to forming a successful business partnership. As well as asking questions and being open to learning more about their business, challenges and goals. This not only helps in future business, as our insight is already deeper but also creates a lasting bond.
Personality Matters
Take the time to get to know your clients on a personal level. Ask questions not only about their business goals and objectives but hobbies, interests and families as well. And really listen to their answers, as you never know who likes drone racing or goes to the same gym as you. This helps us develop a deeper understanding of our client’s needs and creates strong relationships.

Offer Extra Value
Once we have a better understanding of our client’s needs, it is time to start offering them tailored solutions to help them meet their goals. We believe in providing services that go beyond what our clients are expecting. Showing that we’re willing to go above and beyond helps us stand out from the competition and create a strong bond between us and our clients.
Build Trust
Trust is key to any successful business partnership. We are honest and open with our clients and make sure to deliver on our promises. This helps us build two-sided relationships with our clients, allowing us to communicate openly and freely.
Value Input
We facilitate product feedback through various channels i.e. data analytics or user feedback during live sessions. We sit down with our clients, go through relevant data, then combine it with the client's domain expertise and at the end, plan together what to build next.
By following these practices, we were able to build successful business partnerships with our clients that go far beyond sales and help us grow.
If you are interested in discovering how we approach our business partners, do not hesitate to get in touch.

Cleevio Rebranding: Why We Decided To Change
Today, we’re updating our website and brand to better reflect what we stand for at Cleevio. And even more importantly, what we can do for our clients.
Why
Cleevio always has big plans and aspirations. From building excellent digital products to creating a great place to work. As we evolved, the change was needed at last - not only in the brand but also in our services, which are now oriented towards long-term partnerships and focusing on the best value we can bring.
This growth, combined with the opening of our new office, meant that our website and branding no longer represented all the things we are doing and stand for.
Defining Cleevio
As it turns out, everybody at Cleevio had their preferences about how we should present the company. This creates a tangled web of powerful opinions on visuals. But these can be untangled – if you take a step back. To tell our story to the world, we first need to sit down and define it for ourselves. For that, we partnered up with Lukáš Burkoň, the leader of our new division, Cleevio Growth.
We conducted a workshop series on value propositions, customer profiles, and internal goals (just like we use with our clients, but for our own company!). With everybody on the same page, we achieved alignment across the company on the values and goals we should represent. Using the StoryBrand and brand archetypes frameworks, we crafted our tone of voice and brand personality: Creative Sage. Inspirational, guiding, articulate and knowledgeable.

Going Visual
How do you translate “Creative Sage” into a visual identity that can be used across a multitude of media and platforms? After all, these are two personalities that don’t necessarily match together on first look. We simply broke down what each could mean and represent.
- Sage: wise, minimalistic, lightweight, subdued, sharp, professional
- Creative: engaging, playful, inspiring, collaborative, a bit funky
Then it just meant iterating over your usual visual brand components like typography, colors, imagery, layouts and supporting elements.

Explaining What We Do
In addition to having design and engineering hard skills, Cleevio also strives to partner up with clients on the product side of their business. In the end, that means creating the best digital products with the most impact. For that, we need to explain the value we bring to the table.

Even the most powerful idea is fragile at first. However, with our 360 process, we can carefully capture it, mold it, and turn it into a successful statement of the client’s vision. Validate, prototype, test, iterate and develop. An excellent product is born.
We tapped into the 3D skills of one of our talented designers, Adam Kozel, to bring this to life. The idea and product are represented by glass structures, while our battle-tested process is visualized by solid black metal framing. On top of that, we use vibrant cursors to show the collaboration between us and the client – a key concept in every product we build.
Looking Into The Future
Launching our updated brand means a lot to us. It is as much a snapshot of where Cleevio is today as it is new ground for us to build on. Most importantly, it gives us a solid foundation to continue building true product partnerships with our clients.


Brand Evolution: Apps Builder to Digital Product Innovation Partner
Cleevio, as a software development company, has been providing businesses with innovative solutions since its beginning. From product-based software solutions to becoming a company that focuses on building business partnerships, Cleevio has undergone a significant transformation. In this article, we will explore how Cleevio has evolved and why we believe in building business partnerships as a crucial part of our future success.
Early days
The early days of Cleevio were focused on developing and delivering product-based software solutions - mostly custom-built mobile apps. This was a common approach for many software companies at the time. However, as the company matured and the market evolved, it became clear that this approach was not sustainable, nor did we like it. The market was becoming increasingly competitive, and there was a need for new and innovative solutions that could help businesses stay ahead of the curve.
Transformation
In response to this, we slowly shifted our gaze and started focusing on building business partnerships. This meant that instead of selling products, we are collaborating with other businesses to provide custom solutions. This approach enabled Cleevio to understand the needs and requirements of each customer and provide a tailored solution that met their specific needs.

Stronger relationships
Our focus on building business partnerships has resulted in the expansion of long-term relationships with our customers. This has helped us increase customer satisfaction and retention, as customers really appreciate the personalized solutions tailored to their specific needs.
"Our team is dedicated to building strong business partnerships with our customers. We understand that every business is unique, and that is the reason why we work so closely with our partners. Our focus on collaboration and partnership has allowed us to remain at the forefront of innovation and provide the latest and most advanced solutions. Together, our team and our partners are driving success in the industry." - David Bezděka, CEO @Cleevio
New business partnerships
Our focus on business partnerships has allowed us to expand our reach and customer base, as it continues to help us work with a growing number of businesses to provide bespoke solutions. By working with others, we increased our visibility with partnerships and reached a wider audience.
Innovation
Another advantage of building business partnerships is that it has allowed us to become more flexible. By working with other businesses, Cleevio has been able to develop new products and services that are better suited to the needs of its customers. This has allowed us to remain at the forefront of innovation and provide our customers with the latest and most advanced solutions.
We see our evolution from a product company to a company building business partnerships as a step forward. By focusing on building strong relationships with our customers, we increased customer satisfaction and retention and expanded our reach and customer base.
We believe that our approach to business partnerships is a great example of how companies can evolve and adapt to changing market conditions.
If you are interested in learning how business innovations can help your business, do not hesitate to get in touch.

Binance Breakfast: Initiative for a Bright Crypto and Blockchain Future
The first of many Binance Breakfasts took place last week in our Prague offices. The event brought the community together to discuss the future of blockchain and crypto in the Czech Republic. Binance's goal is to educate and empower the community, making the country a hub for innovation in the crypto space.
The first half of the event was a presentation of Binance's vision and values as they shared their insights and perspectives with attendees.
The second half of the breakfast was dedicated to networking and building connections between attendees, fostering an environment of collaboration and community. Binance believes that by working together, the crypto and blockchain space can become a safe and accessible place for everyone.

We at Cleevio are passionate about improving the crypto and blockchain industry in the Czech Republic, so we are happy to have helped make this event a reality. We have ambitions to support Binance with market education, consulting local companies and helping develop local BSC projects.
The first Binance Breakfast was just the beginning, and we hope that participants from the banking, fintech, news and web3 scene left the event feeling inspired and motivated.
The next event will be next month. We will focus on creating an action plan to turn these goals into reality. We can not wait to see the progress made in the Czech Republic. With the collective efforts of the community, the Czech Republic has the potential to become a leader in the crypto and blockchain space, and this Binance Breakfast event was just a first step towards that future.

How the Software Product Development Process Works: A Complete Guide
Software product development is a challenging discipline with a clear goal: to deliver a certain product with the help of information technology in the best possible quality and of the greatest possible benefit. All within a defined time and budget, of course.
Building quality products is impossible without a solid partnership between the client and software vendor. I’ll explain why this aspect is critical and share some practical product development tips in this article.
I’ll also walk you through the stages of the product development process, and give you an opportunity to get actively involved in one of them.
Sounds interesting?
Let’s dive in!
Before You Kick Off The Product Development Process
The very first step of the process is defining a clear product idea and selecting a quality product development company.
Once you know exactly what kind of product you want to develop, you’ll need to team up with an experienced product manager to discuss your vision. Remember that it’s you, the client, who’s at the heart of the process- you have the best understanding of your product and a plan for its future.
The product manager will help you translate your vision into reality. Together, you will start with the preparation phase which can include outlining implementation plans and defining product roadmap.
However, the phase might also reveal potential risks, such as low market demand. A good product manager will let you know how much potential your product has and if there is enough of a niche for it to address.
Pro Tip
Don't be afraid to communicate openly with your product manager!
Remember that you can base your cooperation on an NDA contract that guarantees confidentiality on both sides.
After all, the more information you share with your product manager about your business, product, and plans, the more fruitful the collaboration. The product manager will then have a better understanding of your vision, and will be able to incorporate it into every stage of product development. This, in turn, will make the whole process much more efficient.
For example, if you want to go global with your project, it’s a good idea to plan for multiple languages, even if at the beginning you will be using just one.
If you want mobile development, then according to the plans for the future, you can either go with multi-platform development such as Flutter (in the case of a simple application that will remain in a defined scope in the long term) or with native platforms such as Android or iOS (in the case of a complex application with 3rd party integrations).
All of this proves just how important it is to share your full product vision with the product manager.
How Does Software Development Work In Practice?
As mentioned above, a solid, trusted partnership between the client and a product development company is essential to building a successful product.
Here’s what the software product development process consists of:
- The initial stage - your target audience group, value proposition, and high-level user journey are defined, market analysis is specified, product design is prepared, research is done, and solution concept is defined
- The development stage - the technical preparation of the project starts, the infrastructure is created and the actual development and testing begins
- Pilot stage (recommended) - a pilot for beta users is launched which is later transformed into a live product for everyone
- Maintenance stage - once the project is completed, you need to take care of it, either by maintaining it or developing additional features
The project may involve multiple experts, such as:
- Developers (many platforms) - responsible for software development
- Quality managers / testers - take care of testing and the overall product quality
- Solution architects - define the most appropriate technical solution based on the product requirements
- Analysts - define the foundation of the product and prepares the specifications
- UX/UI designers / graphics - prepare the visualisation of the product with the user experience in mind
- DevOps engineers - if the solution is complex, infrastructure experts need to be involved to take care of the infrastructure, set up proper monitoring, prepare the system for a heavy load, etc.
- Project/product managers - collaborate directly with the client and manages the team to complete the project effectively
Pro Tip
Make sure you work with people with different areas of expertise!
Involving proper experts at the right time will make the product development process much more efficient, and help deliver a better solution.
Every single expert offers a unique, valuable perspective. For example, UX/UI designers don’t just deliver visuals, but produce designs aligned with user behavior which facilitate product adoption.
Quality managers bring a structured, methodical approach to uncover hidden bugs. They provide essential guidance to developers by pointing out error types, locations and circumstances under which they occur. Thanks to their input, developers can fix the errors early on and proceed with building further functionalities. Quality managers’ focus on detail makes them responsible for deploying a stable product to all users.
Speaking from experience, involving the right people at the right time is absolutely essential from a strategic point of view. Each product is different and might require support from different experts. So, make sure you agree with your product manager on what experts you’ll need, even if just for a single consultation.

Software Product Development And Its Stages
We’ve outlined the stages of product development above. Now, let’s dive deeper into each of them and see what exactly they consist of:
1. The Initial Stage
Although critical, the initial phase is often neglected. It includes mapping out of the product definition, specification, design, and solution concept. All of these elements should ideally be based on real user needs verified by a thorough market research.
For the most effective product development, you need a well-written specification covering two levels:
- Business level, where the use cases are defined
- Technical level, where the approach to the development of use cases is defined
If you’re building a mobile application, web solution, game or any other product requiring a user interface, it is also necessary to have a visualisation - at this stage, you’ll need the support of a UX/UI designer who will prepare intuitive designs making your product easy-to-use.
In the initial phase, you should also dedicate time to market research. It will help you define target groups, understand their needs and find correlations between the two. The data you’ll collect will show the real market demand and user expectations.
Unfortunately, due to its cost, market research is often overlooked. Yet, it is essential to the success of your product. It will help you to not just discover and understand your target market, but also better allocate resources, making the product development process more efficient. It can also help you gain advantage by aligning your solution with user expectations better than the competition.
Remember that it is not always necessary to commission the research from a large agency. You can conduct the research yourself using a basic questionnaire that will help you collect the essential information.
If you create a questionnaire yourself, make sure to structure it properly with your target audience in mind. Set a clear number of respondents you want to reach and decide whether you’re going to survey the general public or experts only.
Don’t include too many questions and put the most important ones on top - this way, you’ll get the most critical data while the respondents are still paying attention.
Here’s an example of a questionnaire that one of our partners is currently using for market research - feel free to use it as inspiration, and take the opportunity to actively participate in the initial phase by helping us with your opinion!

The market research insights should be discussed with a solution architect who will recommend the best technical solution based on your brief.
It is key to define the right technical solution at the very beginning of the process, so don’t be afraid to share your vision with the architect. They will present the right approach, and ensure that no major technical changes will need to be made halfway through the project. They will also advise on a security approach that will best help protect your solution.
Pro Tip
Very often, the roles of a graphic designer and a UX/UI designer are misunderstood.
A graphic designer is a visualisation expert who works on user interface. A UX/UI designer focuses both on user interface and user experience.
UX/UI designers combine the in-depth knowledge of design principles with an understanding of user behavior to create a user-friendly experience. They put the elements of your product design into an order that’s aligned with user behavior - as a result, your application will, for example, have appropriately placed buttons or optimized number of clicks leading to a goal completion.

2. The Product Development Stage
The next stage is the actual development of the product that should start with the preparation of the infrastructure.
The preparation includes an efficient setup of the development process that could automate the frequently repeated operations to help save time and move forward faster. It involves setting up the environment/s, infrastructure, security and monitoring in preparation for the actual development.
The development of your product is usually completed in parts. These are delivered in shorter time intervals called sprints. A single sprint starts with planning where tasks for a given time period are defined, and later introduced and developed. Developers meet regularly to address technical issues and potential complications at regular short meetings called standups.
Usually, before the end of a sprint, developers prepare a testing version of the product. The QA team puts it to the test and reports any malfunctions and bugs that the developers need to fix.
The way testing is done depends on the type of your product. In the case of a mobile application, developers have to build code and transform it into an installation package that the tester later downloads, installs, and tests. In the case of web solutions, developers need to deploy the code into the right environment where tests can be conducted.
Depending on the team size, product type, and delivery date, testing can be done in batches, continually, or automatically.
The development stage is one in which you can get involved. After the team completes the development and testing of the demo version of your product, they will present the progress to you. Once this is done, you can examine the features yourself and share feedback with the team. It is best to do product demos on a regular basis to monitor the growth of your product and actively test it yourself.
This collaboration between you and the development team improves the product. You can consistently get an insight into what's been developed and at the same time consult future features or validate best practices with the experts to get your priorities right.
The last stage of testing involves deploying the solution to the right environment where it can be tried by real users.
A sprint ends with a retrospective where the team discusses the ways to improve the solution and collaboration, and plans improvements for the next sprint.
Pro Tip
Testing and deployment are two important areas where automation can be introduced. Doing so usually means a higher effort at the beginning, but subsequently a greater benefit in the future. Manual, repetitive activities can then be solved with one command line or within one click of a button which saves your team’s time and gives them space to focus on other product areas.
On the other hand, your team should only automate where it makes sense. For example, automated deployment, automated login and logout scenarios are usually more efficient.
But because automated tests are performed without human intervention, they cannot replace human evaluation. So if your solution contains functionalities that cannot be accurately described, automating them might not be the best approach.
The best practice is to let DevOps engineers decide what stages of the product development process could be automated and with what tools for optimal results. Other experts will surely find something to automate too, but please always consider added value before introducing automation.

3. The Pilot Stage
I highly recommend starting with a pilot/beta version of your product before full launch.
The opportunity to test the product with several real users and getting their feedback is key to understanding if and how your solution resonates with the target market. The pilot stage also allows you to assess the stability of your product and catch any remaining bugs early, when their impact is still relatively small.
Once your product has been beta tested, it’s time for the big release!
Pro Tip
If you’re not short on time or budget, always plan for the pilot stage. The insights about usability and functionality it provides are invaluable, and can save you a lot of headaches in the long run.
If you don’t have resources to run a pilot, use the data you gathered through testing, and openly communicate to users any shortcomings like missing features. This way, you’ll set proper expectations and minimize the risk of potential complaints.
4. The Maintenance Stage
After deployment into production, the product is available to potential customers. At this point, you can either let it run or continue with developing further functionalities. It is highly recommended to closely monitor the product’s performance and user behavior with analytical and monitoring tools.
Analytical tools help us understand user behavior and therefore better tailor in-demand functionalities - and even marketing campaigns! Monitoring tools, in turn, allow us to keep an eye on the infrastructure and take preventive measures against any crashes, so we can either stop them before they occur or, if they do happen, introduce fixes before the errors affect a large group of users.
In the maintenance phase, you can also do another market research to determine what other functionalities might be a good addition to your product.
Pro Tip
When the product development process comes to an end, two things are often overlooked - documentation and support.
Proper documentation should be an integral part of every solution. As people change teams and projects, the documentation is often the only remaining source of truth about the product - that’s why it should be updated on a regular basis and stored safely.

Support is essential in the post-release phase, as it provides hands-on help with any product issues or upgrades. In the best case scenario, you should have onboarded a dedicated team that is ready to jump in whenever help is needed in the development stage. This minimizes the impact of production issues on your users.
Otherwise, if no further development or support is available once your product is released, you risk running into technical or regulatory issues. The IT world is rapidly evolving - there are new technologies or privacy and security regulations that require your product to be regularly updated. Besides, even the most stable solutions need proper maintenance.
So, always keep in mind the future of your product and the fact that it is going to require good care and upkeep.
The Takeaway
Summing up, when it comes to the product development process, the partnership between the client and the provider is absolutely essential to successfully turning your idea into a product.
Do not underestimate the importance of preparation, and always do proper research, involve UX/UI designers, and openly communicate your vision - all these aspects help to define the right architecture of the solution.
Remember that the developers will deliver your product in smaller bits, so stay in touch with your product manager to regularly monitor progress and ensure that you continue on the path you want to take.
Before you launch, test your product among a narrow group of users to collect feedback and detect any errors. And don’t forget about documentation and support that will help keep your product cataloged and up-to-date.
The best part of the whole product development process is the fact that you can create solutions that save time and money, make processes more efficient, and overall contribute to making our lives easier!

Remote Work Abroad: 5 Things We Learned From Our Portugal Stay
In the flurry of recent job market trends, one, in particular, stands out - remote work. Fueled by employees’ desire for more freedom and work-life balance, working remotely has become a go-to model in many companies around the world. At Cleevio, we took remote work to the next level and went beyond just working from home - we offered our team the opportunity to work from a comfortable villa at the ocean coast of Portugal for 3 whole months!
Here’s what we learned from the experience:
Staying on top of trends is a given at Cleevio.
We keep up-to-date with the latest technologies, industry news, and developments - and it’s no different when it comes to the most recent work trends.
Two years ago, we introduced a hybrid work model. It allowed us to meet and work together in the office while providing space for concentration and deep work at home office.
The model worked great - but we didn’t want to stop there.
That’s why this year we introduced a unique company benefit: the possibility to work from a spacious villa at the Portuguese coast for 3 whole months.

Cleevio in Portugal, a.k.a. Our Workation Setup
So why exactly did you introduce remote work abroad, you might ask.
There were a couple of reasons: we wanted to improve our teams’ work-life balance, enable them to get to know each other better and combine work with play in a new setting.
So when the opportunity arose to rent a 10-people villa in the historic town of Aljezur, Portugal, we jumped right on it.
We decided to rent the villa for 3 months in the winter/spring season (February - April) to escape the cold and dark Czech winter, and enjoy a milder oceanic climate. Plus, the turn of winter and spring is the prime surf season in Portugal, and we definitely wanted to catch some waves!
On top of that, the villa offered great working conditions. Equipped with high speed Internet, spacious common areas, and a large table that fit multiple monitors, the place had everything we needed to be able to work on our projects effectively.
Oh, and did we mention the solar-heated pool and barbecue terrace?

Over the 12 weeks that the villa was available, 33 people from 9 teams visited and stayed on average between 7 and 14 days, wrote hundreds of lines of code together, and made tons of unforgettable memories!
Here’s what our stay in Villa Berenika looked like:
Remote Working Abroad: What We Learned
We’ve learned a lot from our first experience working remotely from abroad.
Here are our top 5 takeaways:
1. We stayed ahead of the trends in an increasingly remote work oriented industry
While remote work is growing increasingly more common, very often it means that employees can simply work from home.
A lot of businesses still don’t allow for remote work from a different country, be it because of their lack of flexibility, office-based culture or legal and tax issues.
That’s why offering the benefit of working from Portugal to our mostly Czechia-based employees set us apart from others. We were able to not just act on, but stay ahead of the trends, and offer our teams a truly unique experience - which is what Cleevio is all about!
2. We improved well-being, work-life balance, and creativity
After over two years of pandemic-triggered restrictions and uncertainty, it’s only natural that a lot of us were feeling strained.
The opportunity to work and have fun together with our teammates in a relaxed, picturesque setting of Aljezur helped address this issue. It provided an environment where we could simply decompress and enjoy the moment - which, in turn, translated to an improved sense of well-being and work-life balance.
The setting had a positive impact on our creativity too. After all, who wouldn’t be bursting with ideas if given the opportunity to work by a pool, at the beach, or on top of a cliff?

3. We appealed to a large pool of talents
Because the possibility of remote work from abroad is a unique benefit, we were able to stand out to potential job candidates.
Throughout the time the villa was available, we promoted it across our social media channels and in active job postings. The content featuring the villa garnered more engagement compared to other posts, and sparked questions from our target hiring audience which proves that the benefit resonated with the right crowd.
4. We enriched company culture and strengthened team relationships
This year was the first one where we introduced the opportunity to work remotely from abroad. And we quickly noticed how much of an impact it had on our culture. It allowed our teams to get to know each other better, experience new things together, and make great memories - all of which strengthened our bonds and friendships.
The Portugal villa became a popular topic at Cleevio. The visitors have started their own Slack channel where they shared activity updates, photos and videos of their stay on a daily basis. There were also lots of stories shared in person in the office which made us all feel a part of this adventure.

5. We got the job done!
Even though we were almost 3,000 kms away from Cleevio’s headquarters in Prague, Czech Republic, we stayed focused on our projects and got the job done.
Yes, we had access to a pool, barbecue terrace, breathtaking views, and perfect surfing waves - but all of this did not distract us from our goals.
We remained organized and aligned on our top priority: helping our clients grow and succeed, no matter where we are.

The Takeaway
Being able to work together from Portugal was a great experience that taught us a lot.
Staying ahead of the remote work trends, appealing to new talents, and enriching our company culture were just some of things that the “Portuguese workation” allowed us to accomplish.
Without this initiative, we wouldn’t have learned as much about our company and each other - which is why we’re glad that we’ve taken this step.
If you’re not sure if your business should go remote in a different country, we would say a strong yes! Test the idea and see how it works for your business, and who knows, maybe it will become a new staple in your organization?

What Is a Soulbound Token?
If I told you that there is going to be something opposite to the NFT called SBT… What would you say?
In May 2022, E. Glen Weyl, Puja Ohlhaver, and Vitalik Buterin published a whitepaper titled Decentralized Society: Finding Web3's Soul. They represent the DeSoc (Decentralized Society) and the Soulbound Token (SBT).
Let's take a look at what a Soulbound Token is.
To explain the mechanics of a Soulbound Token, it is best to compare it to an NFT. If you own an NFT, you can sell it. You can send it to your friend. If you decide you want to buy an NFT, you can just buy it.
The idea behind SBT is that you can't buy it. You can't send it to anyone. Only someone else can issue an SBT to you - Soul accounts exclusively can issue SBTs to each other. The SBT can also be removed by the Soul account that issued it.
Let's summarize what a Soulbound Token is:
It can't be bought.
It cannot be sent to anyone (non-transferability).
It can only be issued by one Soul account to another (the idea is that users should have multiple Soul accounts, for example a medical, work, or education account, etc.).
It is possible to remove a Soulbound Token from the account that issued it.
As with the NFT, it is a blockchain token.
What are the use cases of Soulbound Tokens?
Education degrees
Imagine a world where you have a Soul titles account that would enable anyone to verify if you have actually graduated from a given school. There would be no need to prove your diploma validity. The diploma could not be falsified. It would simply be possible to confirm if a diploma was issued by, for example, Harvard University.
Certificates
After taking a course, you wouldn’t receive a paper certificate, but an SBT would be issued to your Soul account. Driver’s licenses, ID cards, etc. could all actually be SBTs. The state would have its own Soul account from which it would issue SBTs.
Medical records
In the case you change doctors, there would be no more need to forward your medical records from one doctor’s office to another. Instead, all of your medical information would be stored in your Soul health account - not accessible to the public, of course.
Job records
Imagine a digital resume with a confirmation of your job tenure from all the companies you’ve worked for. It would be impossible to introduce changes to the confirmations, so there would be no false information about past employers and experience.
A few more ideas
Birth certificates
Olympic medals
Criminal records
Albert loyalty points 🙂
Your imagination is really the only limitation to where and how such a token could be used.
What happens when you lose your Soul account
The solution could be the so-called social recovery. Simply speaking, it means choosing the people or organizations you trust in your immediate area. These entities will then own the private keys to your Soul account.
In the event that someone steals your account, the entities will be able to recover it. Of course, if they "died out,” you wouldn’t be able to get into the account. It is not a 100% problem free solution.
The Takeaway
If you're interested in SBT, be sure to read the above mentioned whitepaper.
The idea of SBT sounds very interesting to me and even though it might seem sci-fi now, I believe the future is digital and a fully digitalized world is inevitable.
So far, SBT exists as an idea and no technical details have been specified. According to an interview with Vitalik Buterin, SBT should appear at the end of 2022. Will it become another big trend? Who knows. But you definitely can’t go wrong by exploring SBTs now.
What do you think about SBT? Can you think of any other use cases for it? And can you imagine that it will become the norm over time?
Fun fact for the very end: As Vitalik Buterin revealed in one of his tweets, the name Soulbound comes from the game World of Warcraft.
Disclaimer
This article was originally published on David Tilšer ’s LinkedIn profile. Follow David on LinkedIn and Twitter for more blockchain insights and articles.

The Power of Static Dispatch in Swift
Apple has made considerable claims about how fast the Swift programming language is supposed to be, stating e.g. that it is up to 2.6x faster compared to Objective-C.
The details of tests behind those claims were never disclosed and those who verified them themselves found quite opposite results – Swift can be by an order of magnitude slower compared to Objective-C when the code is non-optimized. With optimizations, it is still usually slower, but the difference is not as great.
In his book, iOS and macOS Performance Tuning, the software engineer and researcher Marcel Weiher found that there is a way to make Swift run much faster – but one would lose all the convenience Swift brings while going into pure C or C-based functions.
At Cleevio, we strive to write code running as fast as possible while also maintaining convenience, safety, and readability. Swift is therefore great for most of our use cases (although we also made a C-library for a computation-intensive task for one of our projects), as it meets our needs without having to go pure C-blast.
There are, however, many ways to tackle certain code problems, some more and some less performant. This article focuses on the difference between static and dynamic dispatch. We'll look at it in code and cover the performance implications of its use. We will also discuss the limitations in situations when it’s much less convenient to write the code optimally.
Static Dispatch
Static dispatch describes a situation in which we know what function body will be run in compile time. It’s usually represented by a concrete type or generics.
It could be shown in the following example:
Here, we can be sure that DinnerServer will be the one executing the function and the value we will receive is true. The same applies to classes that are notated with final keyword – they cannot be subclassed, so we are sure during compiling what will be executed.
We can see similar results when we use generics that take advantage of Swift protocols to the limits. Generics are an essential part of making an app with Swift, as the language itself was described to be a protocol-oriented language. Let’s define a protocol first:
Now, we will make a conformance of DinnerServer to this protocol. It is a simple task as the DinnerServer already implements the function.
Let’s say we have a function that takes a Server as an argument and returns the information whether the food is ready. It could be defined like this:
In current implementation, however, we say that it could be any Server. We will get the results, but we don't know what type will do the function and as any Server can be used as variable in the function, we do not even know whether we will receive true. Fortunately, there is a way to tell a compiler what type will be responsible for executing the function using generics.
Here we provide T: Server to the function. This syntax in angle brackets means that we do not expect any Server, but one in particular. The compiler would then create separate versions for every type that we provide in the function in our codebase. So, this will be a very slow margin increase in the size of the compiled application but we can be sure that the execution will be fast, as the compiler would create for us a function that looks like the following:
We can’t see such a function in our codebase, but I present it to you so that you have some mental image of how the compiler can benefit from generics behind the scenes.
We would of course be able to do the same – just declare the isFoodReady(server:) function for every type that conforms to the Server protocol, but that would be an unreasonably wasteful use of our time. Writing it ourselves would also be more error-prone as it would be easy to forget to create it.
Also, it seems feasible when we talk about one function that we would need to create. But what about some more complex code? For example, if we change the Server protocol requirements to include also prepareIngredients and finalizePreparation functions
and then extend our DinnerServer so that it still conforms to the Server protocol:
We can then create a special function for preparing food that takes care of calling both functions like this:
As you saw in the implementation, finalizePreparation for DinnerServer is empty, as our server does not need to do anything more than just prepare the ingredients. But some other Server may need it.
We can now use this in a different function (orderFood) that should crash if the food is not ready after it is supposed to be prepared:
As you can see, if we were to create functions for specific types, the amount of code we would have to write would turn exponential
together with the growth of the number of functions or the types using them.
Generics also help the testability of our code as we can create mocking types just for testing purposes without the need of creating specific functions for them to use.
Dynamic Dispatch
Dynamic dispatch is the exactly opposite situation - we don’t know what function body will execute in the compile time. So, we cannot help the compiler to create the same optimizations as it can when we use generics.
I have already presented how the dynamic dispatch function would look like in our setting. It would be a function that takes any type conforming particular protocol Server:
The result here is that it requires more work during runtime to execute this dynamic isFoodReady. Assembly code from the compiler tells the story. First of all, generic and non-generic functions have differences in their implementation.
As we can see, there is a special call to __swift_project_boxed_opaque_existential_1 in the non-generic function as this is what we have to work with – existentials. Also, in the calling place of those functions, we can see differences.
Calling our generic function is a quite straightforward code:
While when we try to call our generic function, things start to look more messy:
Most notable difference here is that there is an additional call to execute function __swift_destroy_boxed_opaque_existential_1. This additional overhead is what causes the code to run slower. How much slower? That will come in the next section.
The existential is also the name we can see in Swift 5. 6. context that introduced existential any. It’s just a way of Swift that tries to force us to declare any before any protocol type that we want in a function (for example). This approach should make us more aware of using dynamic dispatch. We can expect to get a warning or error in the future versions of Swift if we don’t use the word any in our codebase. The above-mentioned code would look like this:
Just that simple. All it takes is to add the word any.
Another example of dynamic dispatch would be a class that is not defined as final. The compiler cannot be sure what class is provided to the function – is it the class that is defined in the function, or one that inherits from it? When the class is declared as final, inheritance is no longer possible and the compiler can then create other optimizations. The same applies to specific functions or variables. If declared as final, they cannot be overridden and the compiler doesn’t have to use dynamic dispatch.
Why would anyone use dynamic dispatch then? We will look into that in the section titled Limitations where I’ll show you examples where static dispatch may be – at least – inconvenient to use in some situations.
The Results
In general, we can expect dynamic dispatch to be slower than the same functions that use static dispatch. Those are indeed the results. The question here would be – at what cost? And the answer differs based on whether we are talking about generics or final classes.
Measuring
I will present the results of using static versus dynamic dispatch. For the measurements I created a library that takes functions defined in a type called MeasureFunc (containing function and its name) and the number of repetitions.
It is very straightforward – it repeatedly runs all functions and then appends the result to a dictionary, which is then sorted and the results are then handled – the library can print them as well as save the results as CSV, for example. The average time of each function is then calculated as a mean of the runs, using CAMediaCurrentTime which is much faster to create than Date and should therefore be more precise.
I will show you the results in relation to the fastest function – so, for example, if one function would take 1 minute and the other would take 2 minutes, the first function would be shown as index 100% and the second as 200% – as it takes 2 times more time to execute it.
The code was compiled with release optimisations, but we can expect it to be much slower without them.
I use TestingClass function for all tests that I will present to you, which is declared like this:
It conforms to a protocol Testable, that has only one requirement – to support init without parameters. I’ve used UUIDs because they take some time to generate even though they are value types.
Appending Class to an Array
Code reuse is one of the great ideas of modern programming. We don’t want to repeat ourselves and we want to change, if possible, only one line in one place, instead of one line in multiple places.
First example when I tested statically and dynamically dispatched code was with adding our TestingClass to an Array. Yes, it would be faster to add it to a ContiguousArray, but – let’s be honest here - nobody uses that (and even I do it only sometimes).
The code to add to an array is very simple for generic type, as it is declared by taking one item and appending it 100 000 times.
Using a dynamically dispatched function changes the code, only that we use Testable directly instead of T. With Swift 5.6. we would use any Testable.
The results are not surprising – it is much faster with generic function. How much faster? By about 62%.
Appending Class to an Array with Initialisation
Sometimes, we may need to not only append one specific item to an array repeatedly, but also create it based on some data and create a model from it. The initialization itself would therefore take some time. And even though the appending would be much faster (as we said before, by 62%), the total time of the function should be much lower as the init time should be the same.
The functions are very similar to just append to an Array, with the only difference being that we initialize it when we append it. The generic function looks like this:
While dynamically dispatched looks like this:
The results support what we expected, using dynamic dispatch is 7% slower than using generics, because significant time is used on the creation of the class. If it wasn't a class or didn’t have 5 variables, the dynamic dispatch would be relatively slower (but never more than the 62% that we showed in the previous test).

Class Recursion
We’re not going to talk about any recursion algorithms (that are usually not even the most performant way to perform tasks). Here, I will show you a very simple code of some class that takes a parameter and then in turn hands over that parameter. The usage of such may be some dependency injection – like in coordinators or possibly in some SwiftUI ViewModel handling.
Recursion using static dispatch looks like the following. As you can see, we start with some item that conforms to protocol Testable and then recurse 5 times (starting from 0).
Dynamic dispatch looks very similar, the only difference is that we declare storage to be Testable instead of Generic T.
Again, static dispatch is much faster, in this case by 67%.

Final Classes
I found only a minute difference when I declared classes as final. But it is definitely a part of Apple’s optimization tips you can find on their Github . I haven’t focused much on benchmarking final classes in tests for this article, but we can see that we can gain a little when we use final classes to improve not only our runtime, but also compilation times.
I recommend this great benchmark from Swift forums done by Jon_Shier that shows that declaring classes as final can bring us around 1% of compilation speed. Not bad, right? And since using generics takes a little longer, we could gain what we lost. At least partially.
Limitations
Our iOS architecture is based on MVVM-C. We first provide dependencies to Coordinator, which is then responsible for creating ViewModel, ViewController, and then attaching ViewModel to ViewController (on View, depending on whether it is newer app that uses SwiftUI or older one built purely on UIKit).
It was fairly easy to start taking advantage of static dispatch in coordinators – As instead of:
We now use generics declared as:
That code is even shorter as it’s not necessary to create typealias for Dependencies to use later in functions of coordinator, so that whenever we would need to change the dependencies, we would change it only in one place.
One of the tidbits here is that whenever you try to go for this change of taking generics instead of any existential, you would have to change it in all places – or at least start with some root coordinator and then continue down with the flow. To showcase this, let us declare a protocol Developer:
and a type conforming to it:
This code compiles:
But the following doesn’t. It is due to generics expecting a specific type – and the compiler cannot make a specific type out of existential.
But it's not as easy when we work with ViewModels and ViewControllers. If ViewModel were generic, it would have to be specifically declared in ViewController using Generics as well as in ViewModel itself (you can see it in the following example in the angle brackets where we have to duplicate the code). We would then have to declare what protocols ViewModel conforms to in ViewController, which would then be little harder to refactor when needed.
It’s also not possible to extend a class to be a delegate for objective-c API when that class uses generics, as can be seen in the following example where we’re getting the error „Conformance of generic class ViewController<T> to @objc protocol UITableViewDelegate cannot be in an extension). Yes, it would be possible to solve through another object that would serve as that delegate, but that would require significant changes.
Generics can also become highly complex when we want to have default arguments for Generic dependencies. It’s very easily seen as the context of SwiftUI, but applies to any context that uses Generics. To showcase this, let us create a very easy ContainerView that has two attributes – content and its overlay.
So, what if we want to create some default arguments for it? We would have to extend ContainerView with them. Let’s say that we want to have a default argument for content to be Text and for overlay to be EmptyView.
For Content it would look like this. You can see where Content == Textwhere we declare that this is an extension that applies only when the Content is Text. And also in the init, there is Text with a localized string of „Placeholder“). So whenever you would use ContainerView(overlay:) init, it would show the Text with Placeholder string.
And for Overlay to be by default EmptyView like this. The result is that we can now omit the overlay parameter:
But, what if we wanted to have an init that has two default arguments, Text and EmptyView? We would then have to declare another extension like this. This approach allows us to combine previous two extensions and declare the ContainerView only as ContainerView() without the need to specify any argument:
But what if we had some other arguments that we would want to default? As you can see, the complexity would be exponential here. For two default arguments, we need 3 extensions. For 3, we would need 7 arguments. Here, the discussion should also be about the maintenance, whether a pure Generic approach is suitable from the beginning when the API or app architecture is not finished yet, and might be changed significantly in the future.
It’s worth adding that there’s just no way around it in some places if we decide to use generics. Apple uses exactly the same for their SwiftUI elements. While we can’t see the actual implementation, we can see the headers and names of the functions in the documentation.
There are two arguments for SwiftUI.Label – for Title and Icon. And as Apple wants users to be able to use some convenience inits, they also have two additional extensions (as Apple doesn’t have any default parameters here) containing convenience initializers. For example, in the first extension where Title is Text, there are convenience inits to take String, LocalizedStringKey, as well as String for image or SF Image. As you can see, there is no way to initialize a Label with some View but also a String of an image – that would require them to create another extension. The same applies to the case where we would have Title as a String but Icon being some View. Making API is just hard and you either provide all the convenience inits or make your life easier.
These limitations are definitely not finite. Depending on the context, there may be ways to overcome this. If you have any good examples of limitations or how to solve them, please let me know!
Swift 5.7
Apple has brought many great additions to Swift 5.7. They significantly improved how we use generics and deprecated some of the limitations I mentioned before.
For example, we defined a following function in the first chapter of this article:
In Swift 5.7., all it takes is to write it as following:
The angle-bracket approach that might have frightened potential users of generics is gone – with the use of the same some keyword we know from SwiftUI, now also in a parameter of a function.
I also showed how hard it is to define default arguments for parameters in SwiftUI. Not anymore. For our use case, we needed 4 inits. All it takes in Swift 5.7 is to have one of them.
And most interestingly, it is now possible to use something people from Apple call existential unboxing. What does it mean? We can now use an existential in a generic function. I think it sufficiently illustrates following example:
As you can see, you can have an existential any type that can call a generic function, which – as it always could – then calls an existential function again. This (with the other additions) is the reason why my presentation for team about generics in Swift 5.7. and how to improve our code with what’s to come was called Big Year For Generics.
From Swift 5.7. we can gradually adopt using generics in code, as it is not necessary now to update the whole coordinator hierarchy if we want to start using generics. We can start with just one and continue along the way.
Swift 5.7. therefore greatly improves what we already had, while the previous code still compiles. Just beware of switching to Xcode 14 too soon, it is by no means stable and has serious issues as of the time of writing the article.
Final Thoughts
So, what to take from this article? Try to use the power of static dispatch whenever possible. Swift is a great language for the coder, although it is very compiler-intensive. And the compiler needs help from us, which we can achieve by using generics, final classes, or even some private and other declarations.
But be careful about over-optimizing. As I showed in the Limitations section, there are situations in which it would require significant refactorings of current code – and may make it harder if we wanted to refactor that code in the future. Always think about feasibility as well, as you may burn a lot of your – or the client’s– time to optimize some code. And even though there are significant performance implications, our phones, computers, and tablets are so powerful nowadays that they may not be noticeable to the user. It’s great to optimize whenever possible, as it may even save some battery life, but don’t overdo it.
Finally, I wanted to share some pieces of practical advice. I believe that your code that uses existentials without any will get you a warning from the Swift compiler in the future – and when it does, think about making it generic instead of inserting any in there. When it comes to classes, you can for example use the following SwiftLint rule for getting a warning if your code contains a class not declared as final (as you can always disable the rule for classes that are to be subclassed). You can of course change it however you like, in our case, we use it only for Managers, Services, and Coordinators.