Rebuilding a 10 year old iOS app with Expo
Development••9 minutes read
Doug Lowder
Engineering
The experience of rebuilding an old app with Expo is faster and easier than you think. Learn how this 10 year old app 100% written in native code was rebuilt.

In 2010, I shipped my “Set Dances” app to the iOS App Store.
It is a specialty app intended for people who are training for Irish step dancing competitions (this is the type of dancing seen in Riverdance). These competitions happen all over the world. There are local competitions year round, several regional and national competitions yearly in Ireland, Great Britain, Europe, North America, and Australia, and the World Championships every year during Easter week.
During championship competitions there is a “set dance”, where the dancer selects which tune the musician should play, and the exact speed it should be played at. My app allows a dancer to use their iOS device to play a built-in recorded version of any of the approved dance tunes, and lets the user adjust the speed of playback.
In 2010 the app was completely written in native code (Objective C) and used OpenAL internally for the audio playback. There have been no functional changes since 2013, and it was last rebuilt in 2017 — but it still worked with no issues on the latest Apple devices and the latest version of iOS.
Despite this, I recently received a note from the App Store, saying that because the app was old and not getting enough downloads, it would be removed from the App Store unless I provided an update!
I then set to work to see how easy or difficult it would be to rebuild this app from scratch, using Expo SDK components, and using EAS Build and EAS Submit to ship.
Application rebuild goals
- All features of the original iOS app. As this was going to be an update of the original app, and all existing users would get this update, I had to ensure that no functionality would be lost.
- Avoid heavy use of custom native code. As far as possible, I wanted to see if all features (especially audio features) could be delivered using existing Expo and React Native packages.
- Android support. I have received many requests in the past to have this app available for Android, especially from people not in the U.S., so this was a good opportunity to finally build an Android version.
- TV support. I am the React Native TV maintainer, and work on TV support at Expo. I naturally wanted to see how hard or easy it would be to make the app work on Apple TV and Android TV, explore optimizing the UI for the TV screen and remote control navigation, and try out EAS build and submit support for TV.
Recreating the main screen with the audio player
I started with the with-router-tv Expo example template, as it supports building for both mobile and TV devices. For the audio playback, I added expo-av , to see if it would support all the features I needed without needing any custom native audio code.
Getting most of the main screen working with playback, rewind, and speed controls, did not take long. I thought that it would take a week, but it actually only took 3 or 4 hours! Things that were tricky or tedious to implement 10 years ago are now nearly trivial!
Originally layout was all done by hand in iOS interface files (.xib files), and all the controls carefully connected to the Objective C source code. Small changes were difficult to implement, and each change required a slow app rebuild. Now it’s very quick to do the UI with React, and flex layout and fast refresh make iterating quick and easy.
Originally, the audio was all custom native code written using the old OpenAL library that is still built into iOS. Now, I can build the audio features completely in JS using expo-av, and no special native code is required.
Tune selection UI
The old version of the app used custom native views presented modally. This worked fine, and modal views can certainly still be done with Expo Router. For the new app, I adapted a fairly common navigation pattern, with top level drawer menu navigation, and tab navigation for the tune selection view to show different available lists of tunes. I did add a small patch to @react-navigation/bottom-tabs to allow the tab bar to be on the top of the screen for TV. The new UI came about after several iterations and feedback from beta testers who tried TestFlight builds.
In-app help
This was also done with a custom modal in the old version of the app. I reused the same tab navigation layout for this as I did for the tune selection UI.
Overall navigation structure
Drawer layout turned out to work well for this app on phones — but not on TV, where it led to complexities with focus navigation (navigation around a single screen using the TV remote arrow keys). It was easy to solve this by adding a new top level folder layouts containing phone.tsx and tv.tsx. With this structure, the top level app/_layout.tsx can pivot to different platforms:
import TVLayout from '@/layouts/tv';import PhoneLayout from '@/layouts/phone';import { Platform } from 'react-native';export default function RootLayout() {return Platform.isTV ? <TVLayout /> : <PhoneLayout />;}
The overall route structure of the app in expo-router now looks like this, with the top level index.tsx implementing the main screen, and subdirectories containing the tab layouts and routes for the tune selection UI ((tunes)) and the help screen UI ((info)). Both tab layouts use a common bottom tab configuration returned by useBottomTabOptions.
app:index.tsx_layout.tsx(tunes):_layout.tsxtrad.tsxslowhp.tsxhp.tsxjig.tsx(info):_layout.tsxinstructions.tsxthanks.tsxabout.tsxlayouts:tv.tsxphone.tsxhooks:useBottomTabOptions.tsx
Scaling the UI
For this app, UI elements needed different sizing and layout for all the different form factor requirements:
- Phone portrait and landscape, including phones with large aspect ratios
- Tablet portrait and landscape
- TV screens that are much larger than phone screens
I eventually settled on a simple custom hook (useScale) that all components use. This hook gets the window dimensions and returns all the properties needed for styling.
import { useWindowDimensions } from 'react-native';export function useScale() {// Screen height and widthconst { height, width } = useWindowDimensions();// Scale all sizes, margins, padding, by this factorconst scale = Math.max(width, height) / 1000;// Screen aspect ratio (always > 1)const aspectRatio = Math.max(width, height) / Math.min(width, height);// True for landscape viewsconst landscape = width > height;// This boolean selects for phones that are very tall compared to their width,// and is used by the main screen to lay out some controls more vertically// when in portrait orientationconst tall = !landscape && aspectRatio > 1.7;return {height,width,scale,landscape,aspectRatio,tall,};}
EAS build and submit
For ad-hoc builds, I used local credentials.
For production builds, EAS has a limitation currently where only one provisioning profile can be associated with a given iOS bundle identifier. For an app like this, where the iOS app has a “companion” Apple TV app with the same bundle identifier, this limitation prevents EAS from having credentials for both the iOS and the TV app. I therefore built iOS the usual way, with EAS storing the credentials, and built tvOS with local credentials for production.
EAS submit worked without any issues for both iOS and tvOS, and was exclusively used for uploading new builds for TestFlight internal and beta testing.
Other features
- iOS/tvOS audio routing to Airplay devices: This was the one feature where it seemed that the fastest approach was a custom module. I built a small Expo native view module to wrap the native Apple
AVRoutePickerView— it works well on both iOS and Apple TV. https://github.com/douglowder/expo-av-route-picker-view - OTA updates: The
expo-updatespackage worked well for this app — I used it for testing small UI changes in my TestFlight builds.
Bugs found (and fixed)
A nice side benefit of building the new app was finding and fixing two actual bugs in the underlying open source packages I am using:
- React Native TV had an issue in the
Pressablecomponent under certain conditions: Pressable regression in 0.74.2 expo-updateshad an iOS-specific issue: fix embedded file map for audio issue
Shipped!
The new version of the app was accepted by both iOS and Apple TV App Stores within a day of submission. Most of my previous iOS users have already updated to the new version.
Next steps
My next work on this project will be shipping the Android version. It already works well on a Samsung phone, and am making a few Android specific UI changes before shipping.
It was a really GREAT experience rebuilding this app using Expo! Development was easier, faster, and resulted in a much better and more maintainable app. If you've been hesitating to update an old app I hope my experience gives you the courage to take on the project. This doc is a good place to start.


