Ludovic Frank - Freelance developer

From zero to production: put your Symfony-UX/Turbo application on the App Store (iOS)

ionicons-v5-k Ludovic Frank Sep 13, 2023
93 reads Level: Confirmed

Hi there 🙂,
After last week's slightly lighter article, this week we're going into something a bit harder....

My experience with native and hybrid apps (before Turbo Native)

This isn't my first app on the App Store, whether it's personal projects or client apps, I've already had the opportunity to work on iOS, through Cordova at the time or even React Native, but I've put that part to one side for a while...

100% native applications

In the case of iOS, when you want to work natively, you need to use the XCode IDE, and the language of choice, in 2023, is Swift.
In fact, I've got nothing against these environments, they're great fun to use, and Apple goes to great lengths to make developers want to create on their platform, that's a fact.
But I've never delved any deeper into this environment...

Why not?
Swift can only be used for one thing (even if you can run it on servers), and that's to work in an ecosystem, Apple's ecosystem.From my experience, application validation on the App Store (side loading is not an option in the pro world) is very strict.

Apple has very strict rules, and if you want to exist on the iPhone, you have to abide by them, that's all.

That's why I didn't want to invest time in skills that I can only use under Apple's giro, preferring to deepen my knowledge in technologies that allow me to offer services to more people...

Web applications (packaged with Cordova or Capacitor)

I've spent a lot of time on these technologies, with this application for example, which runs on the Ionic Framework (Angular).

Just like "100% native", I've put this technology aside... it's been almost 4 years since I wrote any code for an app that would end up on the App Store.

Why is that?
Basically, the "webview" does everything, there are plugins, which are written in native code, but as soon as you want to do something a little more advanced, you get the impression that you're tinkering with something... for example, I remember an application in which the user could easily log in by scanning a QR code displayed on his computer... And to display what the camera on the computer was looking for, the user had to write a code.
And to display what the phone's camera was filming, I had to make the background of my (HTML) view transparent, so that the "native view" was displayed.

This is just one example among many, but every time I do something like this I think "Ah yes, that's some high-level tinkering there anyway..."

There's another point that bogs me down: why use the web to make transitions? iOS has its own navigation controller, and it works perfectly, it handles transitions and gives access to gestures that users are used to using, wouldn't it be nice to use them?
I later saw that others had the same idea, and that we were starting to see plugins that could replace Ionic's CSS transitions with that of the OS, but at the time I wasn't interested. I wasn't really interested in this at the time, as Cordova was slowly being phased out and replaced by "Capacitor".

React Native

In fact, in the case of React Native, I criticize it for the same thing I criticize React for: it's a technology, like native, that requires a huge investment, and we see a lot of companies chasing after "React" and "React Native" developer profiles, because they don't have the same skills.React Native" developer profiles, because they started working with these technologies (because that's what developers wanted at the time) and are now stuck in a complex ecosystem, with few profiles on the market (the laws of supply and demand, all that).
In fact, on a freelance mission, I helped out a company that was in this situation (I didn't say I didn't know React, just that when I can avoid it, I avoid it).

On that note, let's get back to Turbo Native, if you don't mind 😁

Which application are we talking about?

As always, my "pilot" app used in production on Symfony-UX/Turbo, and at the time of writing, it's well distributed on the App Store.

Its arrival on the App Store was planned from the start...

In fact, when I was working on this application last year, I was already anticipating its arrival on the App Store, when I was researching "hotwired Turbo", I saw that"Strada (bottom of page)" was under development at the time, even though beta versions were available, I just kept the info in a corner of my mind.

My thoughts before the release of Turbo Native ("Strada")

In fact, at the time, I hadn't even looked at the work done by the Turbo team on "Strada", I only had my knowledge of what Apple expects of you when you want to publish an application on the App Store.

Turbo and transitions

I really like Turbo, but there's one thing that bothers me, and that's that when you click on something, or validate a form, there's a moment of "floating", you get the impression that the application is blocked, even if it's less than 40ms, there's no "satisfaction" that the action has been taken into account.

So, on Vélo en France, I came up with my version of Turbo, a version that, whatever happens, gives the user information on the state of the'll always get a "spinner" informing you that "it's loading", so even if your connection isn't crazy, you'll always have a feeling of fluidity.

On the reservation application, there's also this "spinner", but it's not that advanced, I haven't directly modified Turbo to do it, I use "Turbo Frame" and that's going to be a problem for me later on.

And why am I taking so much trouble with these transitions?
It's very simple: try to put an application without a clean transition on the App Store, and you'll simply be rejected...
"We don't want a website, we want an app."

The thing is, I anticipated a problem I wasn't the only one working on, in the case of "Turbo Native" on iOS the transitions are managed... we'll come back to that later 😁.


Unlike a "classic" application where the server just sends back JSON, and any graphics are handled by the client side, "Turbo" assumes that we're using the "templating" engine, which is on the server side...

So what?
When it comes to checking the application, Apple's teams are located... in California, and the servers that "create" the views? In France.

So, to sum up, when they test the app, their requests cross the United States from east to west and um... an ocean 🤣, latency-wise, we're good there aren't we?

Control what you can control ...

The thing is, the network, I don't have a hand on it, I control the client and the server, what happens between, apart from my eyes to cry, I've got nothing.

On the server side, you need to respond as quickly as possible, and for that you need optimized code. We've got PHP that takes full advantage of OPCache, Redis and that kind of technology, or... "Varnish" 🙂.

On the client side, Turbo already has a snapshot system... what if we modified it so that when a user launches the application, we already cache certain things? 😮

These are basically the things I had to think about, before seeing the work done by the Hotwired team on "Turbo native". 🙂

The "Turbo Native" release

Here we are, Strada is here, and this tweet officially announces it. Looking at the github page, we see release 7.0.0.

The discovery

Well, let's have a look at it all. Looking at the doc, it says you have to create an "xcode" project, which means you have total control over the native code...

After an hour of exploring all this, a message on What's app is gone 😮


As you'd expect, the answer was yes.

Preparing your Symfony-UX/Turbo application

Exposing Turbo to the iOS app

First, we need to enable the iOS application to communicate with Turbo:

Yes, ultra-simple, we make Turbo accessible in the "window".

Detect when the client is the iOS application

To do this, we'll create a service in Symfony that checks the user agent for traces of Turbo Native:

You should also create a Twig function that uses, to know when the client is the iOS app.

Also think about the front end, pretty much the same code, but in JavaScript:

By default, Turbo Native doesn't add anything to the user as an agent; we'll do that in the "case study" chapter of this article, below.

Conquering iOS

Come on, let's get to work.

Apple, I'd like to learn the basics of your universe for devs.

Remember above? When I told you about "100% native" applications?
I told you I didn't want to spend time on technologies that only work in the Apple ecosystem, but this is different, I don't need to know how to interface with Apple's proprietary "view" system, in fact I just need to understand a few basics, which took me a total of 30 hours.

The view controller

So, if you want to get started, I'm going to save you some time. You'll need this project: "Turbo navigator ".
To put it simply, "Turbo navigator" includes the code you should have in all your "Turbo Native" applications. Use it, modify it, adapt it; in fact, you can see a "simplified" version in the demo application.

WKWebViews and Sessions

WKWebviews are interface components for native iOS applications. As their names suggest, they are "views" using the Safari engine, but beware: this is not Safari. You'll have to rewrite it 😮 we'll look at this type in more detail below.

Shared pools

What you need to understand here is that, unlike a Cordova application, you don't necessarily have just one webview. For example, when you want to open a modal, you'll load a second webview. But above all, they each have their own cookies, local storage... and so on.

To overcome this, we use a "WKProcessPool", which allows us to share cookies and local storage between different views.

Communication between native code and the web

In some cases, you'll need to send information that will be processed by the native code. To do this, WKWebviews expose JavaScript-aware functions:

note here that "nativeApp" is a variable you choose in the native code.

From the native code, you can communicate with the JavaScript code by using the function :

As you can see, we're actually "injecting" JavaScript, just as if we were doing it from the browser console.


I won't go into too much detail here, simply because in the next chapter, we'll be taking real-life cases, with real code snippets used in production, so I just want to give you the basics: In addition to that, you can take a look at the project on Github and its documentation, but above all at Joe Masilotti's blog, who is the main contributor to "Turbo Native". On his blog, you'll find lots of resources to take you further.

Now, let's take a look at some concrete cases...

Case study

In the previous paragraph, we skimmed over the things you need to know, so there's no point going into detail about things that are already well documented elsewhere.As far as possible, read these chapters in order, because, as you'll see, some things come up again and again, and I won't go into detail every time.

Modifying the Webview user agent

As we mentioned earlier, on the server side (and in JavaScript) we need to detect when the client application is Turbo Native iOS.These three words will tell us whether Turbo Native is being used, and if so, whether it's the iOS version.

To do this, it's in the native code, but you're in luck, it's already done in Turbo Navigator (I told you it was a must-have 😛).

Take a look at this file: at the top, we see the definition of the "string" we're going to add to the "user agent".

Then, further down: "configuration.applicationNameForUserAgent = userAgent".

The "WKWebViewConfiguration" object will be passed to the "WKWebview", which will then add "Turbo Native iOS" to the user agent used for each request.

And you'll see, we use it "all the time".

Open Apple map

Since we're on iOS, when the user wants to know an itinerary (in our case, going to a restaurant) we'll prefer to open the Apple map application, which is on all iPhones.

In the iOS documentation, it is stated that every link passed to the bone containing "" will be opened in the map application.
So, you're probably thinking "well, all we have to do is make a link in HTML to plan, yes, but no, remember, we're in "WKWebView" not in "Safari", if we do that then it will open the link in the "embedded Safari" offered by iOS, and that's not what we want, we want plan to open directly.

Let's start with our "twig" view;

As you can see, here it looks like a simple link, but in fact it's not, this link will be intercepted, it's also code found in "Turbo Navigator", more precisely here

As a result, you need to modify the function, detect whether you want to go to Apple Plan and, if so, pass it directly to iOS.

Here you can see the original Turbo Navigator function, with just an if in front of it. If map is detected, the link is sent directly to iOS.

We could use a Stimulus controller and push code from the webvieww to the native code for this, but that would complicate things...

You can see how it works on this video (Youtube)

Allow the user to call a number

Moving on, it's very similar to what we've seen before, but this time it's going to be a little more complicated. Indeed, if you try to put a "tel" link in your application, this time it'll crash your application altogether, as WKWebview only supports HTTP and HTTPS links by default.

In our "twig" view:

As you can see here, we call up a Stimulus controller, and intercept the "click" on our link, here's the "Stimulus" controller:

Here we use the class I showed you earlier to detect whether we're in a native Turbo application or not.
If we are, then we call the function exposed by "WKWebView", "webkit.messageHandlers.*.postMessage", enabling us to send information to the native code.

Now, on the native side, we'll have to manage this:

In fact, in Swift, we have parent classes designed to listen for events. Here, our parent class is "WKScriptMessageHandler".

As above, click here to see the video (on YouTube)

In an application, if you're observant you'll have noticed that there are animations between each "view", plus there's a whole system of gestures that platform users, for example, swipe from left to right to go back (as if removing the last view from the grid).

Basically, Tuurbo Native simply follows the links that are managed by the JavaScript, after which, in the "basic" version, it's rather limited, so I quickly switched to Turbo Navigator, which handles this really well. when JavaScript makes a "visit", the iOS navigation controller adds a new view to the stack, unless it's in a "turbo frame".

Third-party libraries

It's funny, I was already talking about this in my first article on Symfony-UX/Turbo, but with Stripe I had another little problem...

In fact, Stripe element, once the "setupIntent" creation process is complete, will use a "window.location" to load the "destination page", and that in turbo native by default would open Safari and, if you use the Turbo Navigator overlay, it would open an embedded Safari "in app".
So the best thing to do is tell Stripe not to reload the page, and do a "Turbo.visit" ourselves:

And there's no problem, well, I'm using Stripe as an example here, but in fact, in the booking app, Stripe isn't a traditional "navigation stack".

During development, I had a problem: in the reservation web app, I already had my method for managing transitions. Well, in the web there's no "native" code to manage that, so it's up to us to manage it.

The problem was that my code to manage this was compatible with a classic navigation stack... In fact, even adapting my web code, the fact of having the "back" button in the order-taking wizard made for a completely lousy user experience.

To understand what I'm talking about, you can watch this video on YouTube.

On the video, you can see that when I "tap" on the button to reserve, rather than navigating "forward" and changing the whole window, a simple window (a modal) comes on top. In fact, this part is simply managed by Turbo Navigator, which needs to be supplied with a .json file.

A relatively simple file, with patterns and instructions for these patterns, which you can read more about in the documentation.
Once this file has been created, you can either host it on your server, or, as I've done for the moment, package it in your application. Once your file is in your "XCode" project, add it to your "SceneDelegate":

All that's left is to adapt your file according to the documentation, so that it works the way you want it to.

Native sharing

Ah, reminiscent of one of the first articles I wrote on this blog...
Except that, as I've said several times, this isn't Safari, and "window.navigator.share" doesn't exist, in fact as I'm writing these lines I'm thinking we could "re-create" it so we don't have to modify the existing code, but having 100% of the conrol on the application code, I've done it differently, so hang on, it's a bit more complex.

To start with, in twig, we're going to use a Stimulus controller:

Yeah! we pass it a lot of info, but it's just a lot of data, it's nothing.

Next, we have a classic Stimulus controller:

Here you'll recognize the little "...nativeApp.postMessage", passing the data to the native code.
We also pass a "modalController" variable, which tells the native code whether our controller (the iOS navigation controller) is the "basic" one or the "modal" one.

Important for what follows: as you can see in "Turbo Navigator", there are two controllers, one for "basic" navigation and one for "modal navigation".
For the interface creations that follow, iOS asks from which controller it should create these interfaces, so our code needs to pass whether or not we're in the modal.

For simplicity's sake, I'm going to put everything in the "TurboConfig" class, but once you're used to Swift, you'll understand that we need to separate the classes:

The first difficulty here is that we need access to the controllers, which is why everything is in TurboConfig... it's up to you to clean it up 😁 but I'm assuming that, like me a week ago, you know Swift by name.

Here, I'm not putting a video, because you've already seen what the iOS sharing interface looks like in, that article, it's exactly the same interface here, except you can't call it via "window.navigator.shar"

Displaying the email dialog box

On iOS it's not uncommon to see system elements embedded in applications, for example an "embedded Safari" is quite common when you open an HTTP link in applications.

It's a little more difficult than what we've done before, because there's an extra notion to take into account.

In Twig, we're going to add a call to a Stimulus controller on "mailto" links:

Nothing too complicated, you're starting to get the hang of this kind of code 😛.
Now for the Stimulus controller:

Yes, it looks just like all the others, in fact we're just passing the data to the native code.

It's in the native code that things get a little more complicated:

What's new here is the "mailComposeController" method. In fact, this iOS popup calls this method when the user clicks on the "close" button of the email modal, so we have to manage it and simply close it.

As above, I've put everything in the "TurboConfig" class, so it's easier to understand, but once you've understood it, it's up to you to break it down into several classes, and don't forget, "MFMailComposeViewController" also needs to know the view controller from which it's called 😁.
By the way, if you're observant you've seen the NSObject extension, it's a prerequisite of this iOS function 🙂

If you want to see what it looks like, you can watch it on this little YouTube video.

Adding an event to the calendar

Frankly, at first I thought "it must be simple", the "iCal" comes from Apple, right? So there must be a native function that reads these files and basta? ... how naive I was 🤣 !

First of all, in "info.plist", you'll need to add a line to say that you plan to work with the calendar, so in your "info.plist" file, add: "NSCalendarsUsageDescription" with the value "The text presented to the visitor when access to the device's calendar is requested".

Once this is done, in our twig :

There, I see you say, what's "calData"? It's a "JSON" that I generate with all the info that I normally put in the iCal file, so that you can better understand what's coming next, here's a dump :

So, as usual, our Stimulus controller:

So far so familiar ... now let's go native:

Ah that's right, there's more code there...
The things to watch out for, and that I wasted time on, are : creating the event before you have the right to write to the calendar will cause you to "crash" the addition to the calendar, the "state" of the "eventStore" linked to the event, and the "state" of the "calendar" linked to the event.eventStore" linked to the event (yes, you have to keep up) is not automatically updated when you create the event before requesting authorization...

How about a video to see the final rendering?

Changing the view background color

My application is rather dark, to recall the interior of the restaurant. The problem is that when the "WKWebView" is loaded, you see the basic (native) view of the application, and by default, "Turbo Native" uses white as the background color.

To change it, go to this line.
So, apart from forking Turbo Native to modify it, I haven't found any other solution.

Publishing on the App Store

To publish on the App Store, you need a developer account, which will cost you €99 per year.

Once you have your account, go to "App Store Connect" and create your application in the interface, then upload it via "XCode".

The verification process can be quite stressful, but in my case, having dealt with Apple in the past, I know what they're looking at.I must admit that when I received the email telling me that the application had been validated, it was a great relief.


The idea here was to highlight the fact that Turbo Native requires a minimum interest in how iOS works on the Internet in order to publish an application on this platform.

I preferred to use real-life examples rather than talk about theory for a long time 🙂.

Have a great day and see you soon 😁.