Picking a Mobile Platform (Native vs. React Native vs. MAUI)

It all started when I was creating a simple Expo.dev app that helps my children to practice for their spelling tests.

Simple concept: you add a list of words. By default the words are hidden, when you click the play button the word is spoken, the child writes it, then reveals the word to assert the spelling. The problem was the default text to speech implementation (at least on iOS) wasn't always clear on every word. To an adult it would be no problem, but to a child learning to spell, it was.

After comparing a few cloud TTS solutions, I settled on Amazon Polly. Simple API, should be easy to consume in my Expo app. Not quite, as of this writing Expo's file download API doesn't allow streaming, it needs to download the file in full before playing. That wouldn't be that huge of a hurtle except Expo also doesn't have a full crypto API (at least not one I could figure out), so my only choice was to make the Polly S3 links public, which could take a few seconds. Annoying and slow, but it worked.

Around this time MAUI Hybrid was trending on my social media, and I thought "Hey, I'm a .NET develloper, maybe I should give that a try." So I re-created the same application in MAUI Hybrid (Blazor). Saving a file stream was much easier, but playing a local file wasn't quite as straighforward. I eventually got it to work using an audio tag with a src pointing to a relative "public" location in the file. That worked on iOS, but didn't on Android. The point wasn't to make a real production application, it really needed to only work on my Kids' iPads. But it was still bucking.

While looking for solutions I sumbled upon an article on SwiftUI. It's been a couple years since I've written a native iOS app, I haven't been following new features. SwiftUI looked like a much improved way to write iOS applications quickly.

So I re-created this simple app in SwiftUI.

And that got me thinking...

The Challange

If I was to build a new app today. Which platform would I pick? I would consider the following things for my choice:

  • Speed of development
  • Ease of debugging
  • Documentation and community support
  • Stability

I decided to test React Native, .NET MAUI (Forms, not Blazor), vs Native (SwiftUI and Android Compose). I would create 4 apps with identical features that would take me each less than 2 days to build. I mainly wanted to test input forms, UI navigation, maps and custom layers. Things that I would use in my line of work.

Scope

  • Map with 3 selectable layers
  • Map click, add point, and add button to bring up input forms
  • Form to save details on map location
  • Navigation, tabs, or menu
  • List of saved details from map events

React Native

I picked React Native first because it's the one I'm most familiar with. I figured a lot of the logic and approaches will be replicated across the apps, and it would be most fair to start with the one I'm most comfortable with.

I generally will start with an Expo app. And if/when I run into a limitation, I eject to get the full power of React Native. For those unfamiliar with Expo, it's a platform to create cross platform apps built on React Native. It takes away the complexity of bootstraping an applciation, makeing it easier and faster to get started. No need for platform specific package managers and plugin linking.

It started off well, in the first hour I installed dependencies, setup the initial hello world, then added a map with my current location and base navigation.

Getting a map layer data set was slightly tricker than I anticipated... I forgot how little I actually know postgis. Luckily ChatGPT knows quite a bit. I generated 3 GeoJSON files: roads, cadastre, and inspections. The layers ranged from 2,000 to 35,000 geometries per layer.

Expo.dev screenshot 1
Expo.dev screenshot 3Expo.dev screenshot 2

I considered putting the layer selection into either a modal window, or right inside the navigation menu. I decided on the navigation menu.

Checkboxes as menu itmes.

The application quickly turned into a bit of a mess with state being passed through props. It was time for state management. And yes I could reach to what I know, Redux, but I wanted to try something new to me: react-query with async storage. But it was not meant to be, I spent nearly an hour on it and couldn't quite get it to work.

Redux it is. But I did end up learning something new, it's been a while since I implemented redux into an application, and the use of slices was new to me.

Implementing the map marker, button, and listing the form data was fairly straighforward.

Expo.dev screenshot

I will admit the form and the list isn't the best looking. I know React Native has probably the best selection of UI libraries for cross platform development, but I wanted to try to keep the look as much as "native" as possible. The end result just wasn't really great.

<MapView
  style={styles.map}
  initialRegion={initialRegion}
  showsUserLocation={true}
  followsUserLocation={true}
  onPress={(e) => {
    setMarker(e.nativeEvent.coordinate)
  }}
>
  {marker && <Marker coordinate={marker} />}
  {layers.map((l) => {
    if (!l.selected) return
    if (l.geoms) {
      return l.geoms
        .slice(1, 500) // maxes out at 4000
        .map((g, k) => (
          <Polyline
            key={k}
            coordinates={g.coordinates}
            strokeWidth={2}
            strokeColor={l.color}
          />
        ))
    }
  })}
</MapView>

I was a bit blown away that I could just iterate through the arrays of geometries and add them to the native Apple Map using react syntax. I did test the limits though, and was able to consistently crash the app with too much data.

RN Summary

Or should I say expo summary? I didn't have to eject, which surprised me. The expo tooling is top notch and I was happy to stick to it.

Time: 6 hours 37 minutes
Lines: 258 (I put it all into App.js due to lazyness)

Pros

  • Cross Platform, one code base for Android and iOS
  • If you're familiar with react, development is fast
  • Hot reload - I make a change in a nested component, it reflects in the simulator without reseting my state or changing where I am in the application
  • Performance for business level apps, animations, data, events, everythign was smooth
  • Stability - at this point RN is stable, I didn't encounter any crashes

Cons

  • Drawing about 4,000 geometries results in a Maxium call stack size exceeded error. Maybe it's not quite the con (wait until you read the MAUI summary). Performance until the crash was great, it could easily draw 3,000 geometries
  • It doesn't look very "native", other than the map and the navigation menu, the default UI elements are a bit ugly. Could be fixed by adding a library
  • Similar to the previous point, but looks the same on Android and iOS. In an ideal world the application should feel more natural to the enviroment it runs in
  • Not from this experiment, but form experience, if you have to eject from expo and rely on random NPM libraries you will eventually run into RN upgrade issues. Count on wasting a couple days a year updating and adjusting your dependencies

MAUI

I love .NET. There is so much included, between .NET and Entity Framework I rarely need 3rd party packages. Contrast to a typical react/node application, the dependency maintenance rarely is an issue. But what I most looked forward to was the IDE. Visual Studio Code is great, but it's not as good as its bigger brother. So when I started this experiment I was quite excited and had high hopes for MAUI.

If you read the first part of this post, you saw that I wrote a small app in MAUI Hybrid. Hybrid is the blazor implementation of MAUI, it's cool and new, but not ready for production. It's also a webview on top of native code, and since I know I'm creating a mapping application I know I will be capped at 256MB of RAM in the webview. For the number of layers I have, I know that might become a problem. That's why I picked MAUI Forms, and although it's technically new as well, it's based on Xamrin which has been around for years.

Something that I didn't do with React Native is I watched a couple short tutorials on the dotnet channel on YouTube.

Really Good Start

It took me an hour to get drawer navigation, a map, current location, on click marker, and a modal button. The base app layout could not have been any simpler to implement, and the default styling looked better than RN to me.

MAUI screenshot 1MAUI screenshot 2

Listing the layers in the menu was a no go. As I mentioned before it was really easy and fast to setup the default layout, but it's also limited in configuration. Links only. I decided on a button over the map that brings up a modal with the layer selection.

Quicly ran into limitation of passing data around. A ViewModel was in order, not something I'm used to from .NET web development. I read up on it a bit and got a modal that opens and closes up and running. No checkboxes yet. It was time to draw some data.

I'm about 2-3 hours into this process and Visual Studio Mac has crashed or needed a restart already a handful of times. This is nothing like the Visual Studio on Windows that I'm used to. I could have done this experiment on Windows and used an Android simulator, but I just didn't expect this experience to be so bad.

To make things worse it quickly became apparent that the performance just won't be there. Remember how RN crashed at 4,000 geometries? But a lower count still had great performance? In MAUI I couldn't get more than 500 to render, but even getting 400 to render would take over one minute. The smallest test I did was with 50 layers, which would take about 6 seconds to render. Not usable.

MAUI screenshot 3

But probably my biggest disapointment was the hot reload experience. It doesn't exist on the Mac version of Visual Studio. Every single change I made, I had to rebuild the appliation. Lose all my temporary data and start on the default screen. That wasted so much time.

<Grid>
    <ContentView Content="{Binding Map}" />
    <Button
        Clicked="reportButton_Clicked"
        x:Name="reportButton"
        Text="Report"
        Grid.Row="0"
        BorderColor="#2b3c3c"
        BorderWidth="1"
        FontAttributes="Bold"
        BackgroundColor="White"
        TextColor="Black"
        HorizontalOptions="Center"
        WidthRequest="160"
        HeightRequest="50"
        Margin="0,0,0,22"
        VerticalOptions="End"
        CornerRadius="25"
        />
    <Button
        Clicked="layersButton_Clicked"
        x:Name="layersButton"
        Text="L"
        Grid.Row="0"
        BorderColor="#2b3c3c"
        BorderWidth="1"
        FontAttributes="Bold"
        BackgroundColor="White"
        TextColor="Black"
        HorizontalOptions="Start"
        WidthRequest="60"
        HeightRequest="50"
        Margin="10,0,0,22"
        VerticalOptions="End"
        CornerRadius="25"
        />
</Grid>

I probably should have used GeoJSON.NET... but oh well

private Layer JsonToLayer(string layerName, Color color, string file)
{
    using var stream = FileSystem.OpenAppPackageFileAsync(file); // ex: roads.json (from Resources/Raw)
    var s = Task.Run(() => stream);
    s.Wait();
    using var reader = new StreamReader(s.Result);
    string json = reader.ReadToEnd();
    var r = JsonConvert.DeserializeObject<List<dynamic>>(json);
    //Newtonsoft.Json.Linq.JArray
    IEnumerable<IEnumerable<Location>> polygons;
    if (file == "roads.json")
    {
        polygons = r.Select(r => ((JArray)r.coordinates).ToList().Select(
            c => new Location((double)c[1], (double)c[0])
        )).Take(50);
    }
    else
    {
        polygons = r.Select(r => ((JArray)r.coordinates).ToList().ElementAt(0).ElementAt(0).Select(
            c => new Location((double)c[1], (double)c[0])
        )).Take(50);
    }

    return new Layer
    {
        LayerName = layerName,
        IsSelected = false,
        Color = color,
        Polygons = polygons,
    };
}

Time: 8 hours 55 minutes
Lines: 399

Pros

  • Able to quickly spin up a simple app
  • Grid system is helpful with layout on different screen sizes
  • C# and .NET are powerful, the full set of APIs is available to you

Cons

  • Visual Studio Mac - it's not as good as the Windows version. No hot reload, wasted probably hours waiting for IDE and simulator to rebuild.
  • Application would often freeze which required an IDE restart
  • Performance - it didn't come even close to the performance of React Native, and I docked RN points for performance
  • Documentation and code samples - I had to go off Xamrin examples often and adjust for MAUI
  • This is more of a side note, but I'm not aware of Microsoft using MAUI in any production applications except their podcast player, yet they use React Native in several applications
  • Commercial libraries - advanced Map components that support GeoJSON are commercial as well as most UI libraries

SwiftUI

I wrote my first iOS app over 10 years ago in Objective-C, back when XCode's panels were all individual windows. Over the years I kept up here and there with Swift but haven't really done anything other than tinkering.

When I heard about SwiftUI (2 years after its realease) I got excited, it seemed to have simplified iOS development significantly. I came into this exercise having written a simple apps for my kids to practice their spelling.

It took me about 15 minutes to get the project set up, the tab navigation, and a basic map. However when it came to adding map markers it took me nearly an hour. Longer than the other projects so far.

SwiftUI quickly reedemed itself when I added a modal window (using .sheet) in minutes. I moved on to building a form and working on data persistence.

Expo.dev screenshot 1

I was at 3 hours and 17 minutes, and all I had left was adding the geometries on top of the map. At this point it's looking really good for native development.

However, it went all downhill quite quickly. After some struggle and reading docs I concluded that it was possible to add overlaps with SwiftUI map component (at least at the time of this writing), I had to implement the MapKit version. Just compare MapKit Overlays docs to React Native Maps Overlays took me over 2 hours to get the map working.

    func loadGeoJson(_ resource: String,  mapView: MKMapView) {
        // https://jajackleon.medium.com/ios-macos-mapkit-swift-drawing-shapes-on-map-f8f4b4d314ee
        guard let url = Bundle.main.url(forResource: resource, withExtension: "json") else {
            fatalError("Couldn't load json")
        }

        var geoJson = [MKGeoJSONObject]()
        var overlays = [MKOverlay]()

        do {
            let data = try Data(contentsOf: url)
            geoJson = try MKGeoJSONDecoder().decode(data)
        } catch {
            fatalError("Unable to decode JSON: \(error)")
        }

        for item in Array(geoJson.prefix(1000)) {
           if let feature = item as? MKGeoJSONFeature {
               let geometry = feature.geometry.first
               let propData = feature.properties!

               if let polygon = geometry as? MKPolyline {
                   let polygonInfo = try? JSONDecoder.init().decode(PolygonInfo.self, from: propData)
                   //call render function to render our polygon shape
                   self.render(mapView, overlay: polygon, info: polygonInfo)
               }

               for geo in feature.geometry {
                   if let polygon = geo as? MKPolyline {
                       overlays.append(polygon)
                   }
               }
           }
       }
    }

After the map was done I moved on to the layer selector and checkboxes, which wasn't too bad.

In summary, I was dispointed with my implementation speed on SwiftUI. I was expecting to be more than twice as fast at RN based on my pervious (minor) experience. And maybe if I developed more in Swift this would have turned out differently.

Time: 6 hours and 3 minutes
Lines: 768

Pros

  • Performance
  • Native API have more platform specific flexibility (see Map on SwiftUI vs MAUI)
  • UI/UX, feel native out of the box, no need for a library
  • Build-in components
  • Automatic Dark/Light modes

Cons

  • SwiftUI components don't have all the features of UIKit, and when you need those features you can use UIKit from SwiftUI, which is great, but complicates things greatly
  • If I made a cross platform app, it would take me twice as much time to build
  • Loading anything over a few hundred geometries took seconds
  • The preview in xcode is quite nice for smaller projects, but as you're adding complexity it's not as useful as expo's hot reload

Android Compose

My original plan was to build the same app on Android, but the SwiftUI result guaranteed I won't be building a cross platform app in native any time soon. I might revisit this in the future for fun.

Conclusion

MAUI is easy to elimate from my next decision, the documentation wasn't great during the transition form Xamrin Forms and the performance simply wasn't there.

It boiled down to SwiftUI/Native vs React Native. For my next enterprised focused app that requires to be on iOS and Android I will pick react native. React and NextJS experience translates well. Hot reloading and the expo development tools provide a good DX. Community UI libraries and plugins combined with an abundance of documentation makes it hard to beat.

If I knew I would be making an iOS only application, and performance would be critical I would pick SwiftUI.