SpellTales Meets the App Store: What Apple Taught Me About Publishing

app store entry for SpellTales

Get SpellTales here: https://spelltales.co.uk/

Introduction

In the first post, I talked about how I built SpellTales for iOS using React Native, Expo and an AWS backend. This post covers what happened when I took that working app and tried to put it in the Apple App Store. It is part story and part guide, because the review process is not just about code. It is about how clearly you can explain what your app does, how it handles data and how it behaves in different environments. This post explains the SpellTales app store submission and everything I learned about getting an educational iOS app approved by Apple.

My first submission did not pass. The review feedback from Apple was detailed and fair, and it highlighted several assumptions I had made. Along the way I used both Expo’s EAS Build service and local Xcode builds. I ran into build number problems, configuration mistakes and small issues that cost more time than they needed to. My aim here is to write down what I wish I had read before I started.

Preparing your metadata in App Store Connect

Before you can submit a build for review, Apple requires you to complete a set of fields in App Store Connect. It is worth taking your time here because missing or rushed metadata can slow everything else down.

For SpellTales, the key items were:

  • App name and subtitle, for me this was SpellTales and a short line explaining that it creates personalised spelling stories.
  • Primary and secondary category, I chose Education with a focus on children.
  • Age rating and content questions, these drive how the the app is presented and whether it can appear in the Kids section.
  • Privacy policy URL, I hosted this as a static page on S3 and linked it here.
  • Support URL, a page that explains how users can ask for help.
  • App description, a clear explanation of what the app does and who it is for.
  • Keywords, phrases like spelling, reading, stories for kids and similar.
  • Screenshots for the required device sizes, including the main reading view and story generation screens.
  • Pricing and availability, including the default territory and currency.
  • In app purchases, subscription products that are linked to the app record.

There is also a detailed privacy questionnaire. It asks how you handle data such as names, identifiers, usage statistics and diagnostic information. For an app that involves children, this is especially important. I answered in line with how SpellTales actually works, and that formed the basis for later review questions. An added complication was that I decided to include adverts to monetise a free version of the app. This meant using AdMob, which involved providing more information about how AdMob would use personal data, and I had to prove AdMob was configured correctly for a child-friendly app, i.e. no tracking data. This was easier than I thought, as I was just able to link to the AdMob policy pages to satisfy the need. This is also why using pre-existing, public third-party services, such as AdMod and RevenueCat, makes your job much easier.

Version numbers and build numbers

Once the App Store Connect entry exists, you need to think about how you will version your app. This part caused more confusion than I expected.

There are two related values to understand.

  • Marketing version, for example 1.0 or 1.1. This is what users see in the store.
  • Build number, for example 1, 2 or 15. This is what Apple uses to distinguish individual uploads for the same marketing version.

In a simple case, you keep the marketing version the same while you work on a release, and you increment the build number each time you upload a new build for that version. If you change the marketing version, for example from 1.0 to 1.1, you typically reset the build number and start again from 1.

In my case this process was complicated by the fact that version information was hard coded in the Info.plist file. Expo and earlier tooling had created entries like these.

<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleVersion</key>
<string>1</string>

These values override the settings in Xcode’s General tab. I spent time changing the version and build number in the Xcode interface and could not understand why App Store Connect continued to show the old values.

The fix was to either remove these keys from Info.plist or change them to use the build settings instead.

<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>

After that, I could manage versions from the Xcode UI. The practical lesson here is simple. If App Store Connect is not displaying the version you expect when you Archive it (more on this later!), check the Info.plist file first.

Using EAS Build for early TestFlight builds

At the start of the submission process I used Expo’s EAS Build service to produce iOS builds. This allowed me to avoid dealing with Xcode directly while I focused on the app itself.

The basic EAS flow looked like this.

eas build:configure
eas build --platform ios

EAS handled much of the signing and packaging for me. Once the build completed, I could see it appear in App Store Connect and then enable it for TestFlight testing. This worked well for initial testing and gave me confidence that the app ran correctly on devices other than my own.

However, EAS Build credits are finite, and I reached the point where continuing to rely on the service would either mean buying more credits or waiting until the next monthly reset. I had already used more builds than I expected, partly due to small fixes and experiments. At that point it made sense to move the build process fully to Xcode.

Moving to local Xcode builds

Switching from EAS to local Xcode builds meant taking more control, but it also gave me better visibility into what was happening. For SpellTales, the native iOS project lived under the ios directory, using the bundle identifier com.kategawron.spelltales.

To prepare for archiving I opened the project in Xcode and checked a few things.

  • The correct scheme was selected, usually the main app name.
  • The bundle identifier matched the value registered in App Store Connect.
  • The signing and team details were correct, and Xcode showed that it could sign the app for distribution.
  • The deployment target was set to the iOS version I wanted to support.

Building an archive in Xcode

Once the project was configured properly, I created an archive. The steps looked like this.

In Xcode:
  Product
    Scheme - select the SpellTales app target
    Destination - select Any iOS Device (arm64)

Then:
  Product
    Archive

Xcode built the app for distribution and opened the Organizer window when the archive was ready. From there I could see a list of archives with their version and build numbers.

Uploading the archive to App Store Connect

With the archive created, the next step was to upload it to App Store Connect. In the Organizer window I selected the latest archive and followed this path.

Organizer
  Select latest archive
  Distribute App
    App Store Connect
    Upload

Xcode walked through a short sequence of prompts, including:

  • Choosing the correct distribution method.
  • Confirming the signing information.
  • Running a simple validation step on the archive.

Once the upload completed, the build showed as processing in App Store Connect. After processing finished, it became available under the TestFlight tab for that app version.

At this point the build number became important. Each new upload for the same marketing version must have a higher build number than the previous one. If you forget to increment it, the upload will fail and you will lose time repeating the process.

Setting up TestFlight

TestFlight is Apple’s mechanism for distributing pre release versions of an app. Once a build was available there, I needed to set up testers.

There are two main groups you can work with.

  • Internal testers, members of your App Store Connect team who can access builds with minimal extra review.
  • External testers, people outside your team who require an additional approval step from Apple before they can test the app.

For SpellTales I started with internal testers to confirm that installs, subscriptions and story generation worked as expected on multiple devices.

The basic setup process was:

App Store Connect
  Apps
    SpellTales
      TestFlight
        Select build
          Add Group or Internal Testers

From there, I could invite testers, control which builds they received and add notes explaining what to focus on during testing.

Yes, you can see my Sunday name in the top corner. To monetise your app, you need to submit proof of identity and proof of address; hence, I needed my full name. Do not use it. You have been warned.

Adding external testers

When you add external testers, Apple performs a short review of the build before allowing wider distribution. This is not as detailed as the full App Store review, but it does check for obvious problems.

For external testing, I made sure that:

  • The build was stable in internal testing.
  • The subscription and entitlement flows worked correctly with sandbox accounts.
  • Any references to test environments were clearly separated from production behaviour.

Once Apple approved the build for external testing, I could invite families and colleagues to try SpellTales and give feedback.

The first full review and rejection

After several rounds of TestFlight testing, I felt confident enough to submit the app for full App Store review. This is when the more formal feedback started.

The first issue related to the demo account I had supplied.

Guideline 2.1 – Information Needed

We were unable to sign in with the following demo account credentials you provided in App Store Connect.

To avoid delays, it is essential to provide access to the app’s full features and functionality with every submission.

Next Steps

Provide the username and password for a valid demo account in App Store Connect that provides full access to the app’s features and functionality or include a demonstration mode that shows all of the features and functionality available in the app. Note that we cannot use a demo video showing the app in use to continue the review.

Between creating the demo account and the review taking place, I had changed some backend settings. The account no longer behaved as expected. The fix was simple. I corrected the configuration, tested the login on the TestFlight build and resubmitted. The lesson was to treat the demo account as part of the release and test it every time.

Answering questions about analytics and advertising

The next part of the same guideline asked for more information about analytics and advertising.

Guideline 2.1 – Information Needed

Before we can proceed with the review of your app, we need additional information about how it complies with Guideline 1.3.

Next Steps

To help us proceed with the review of your app, please provide complete and detailed responses to the following questions.

  • Does your app include third-party analytics? If so, please provide details about what data is collected for this purpose.
  • Does your app include third-party advertising? If so, please provide a link to the ad network’s publicly-documented practices and policies for kids apps.
  • Will the data be shared with any third parties? If so, for what purposes and where will this information be stored?
  • Is your app collecting any user or device data for purposes beyond third-party analytics or third-party advertising? If so, please provide a complete and clear explanation of all planned uses of this data.

Once you reply to this message in App Store Connect with the requested information, we can proceed with your app’s review.

I had already completed the privacy questionnaire, but Apple wanted a clear, written explanation in the review notes. For SpellTales this meant describing how analytics were used in aggregate, how Google AdMob was configured, how RevenueCat handled subscription related data and confirming that no additional hidden collection was taking place.

If you are submitting an app that involves children, I would strongly recommend preparing these answers ahead of time. You will almost certainly be asked for them.

Adjusting registration to meet Guideline 5.1.1

The second major point in the rejection concerned user registration and data collection.

Guideline 5.1.1 – Legal – Data Collection and Storage

We noticed that your app requires users to register with personal information to purchase in-app purchase products that are not account based.

Apps cannot require user registration prior to allowing access to app content and features that are not associated specifically to the user. User registration that requires the sharing of personal information must be optional or tied to account-specific functionality.

Next Steps

To resolve this issue, please revise your app to not require users to register before purchasing in-app purchase products that are not account based. You may explain to the user that registering will enable them to access the purchased content from any of their supported devices and provide them a way to register at any time, if they wish to later extend access to additional devices.

Please note that although App Review Guideline 3.1.2 requires an app to make subscription content available to all the supported devices owned by a single user, it is not appropriate to force user registration to meet this requirement; such user registration must be optional.

I had taken the view that forcing registration before allowing any purchases would make subscription management simpler and would give families the ability to share access. From Apple’s point of view, especially for a children’s app, this was not acceptable.

To align with the guideline I changed the flow so that:

  • Users could access stories and make purchases without creating an account.
  • Registration was explained as an optional way to sync access across devices.
  • Personal information was collected only when it was genuinely tied to account related features.

Updating promotional imagery for Guideline 2.3.2

The final point was about how I was promoting in app purchases on the store.

Guideline 2.3.2 – Performance – Accurate Metadata

We noticed that your promotional image to be displayed on the App Store does not sufficiently represent the associated promoted in-app purchase and/or win back offer. Specifically, we found the following issue with your promotional image:

  – Your promotional image is the same as your app’s icon.

Next Steps

To resolve this issue, please revise your promotional image to ensure it is unique and accurately represents the associated promoted in-app purchase and/or win back offer. If you have no future plans on promoting this in-app purchase product, you can delete the associated promotional image in App Store Connect.

Here the fix was simply to create a new promotional graphic that represented the subscription rather than reusing the main app icon. It was a small change, but worth noting. Apple does look carefully at how you present in app purchases, not only at the code behind them.

Subscriptions, RevenueCat and testing in production like environments

Behind these review points sat the underlying complexity of subscriptions. In Part 1 I mentioned that I brought in RevenueCat once it became clear that implementing subscription logic by hand was not a good use of time.

During submission and testing I needed to confirm that:

  • App Store products and RevenueCat products were aligned correctly.
  • Purchases made in the TestFlight build led to the expected entitlements.
  • Restoring purchases behaved sensibly on a new install or a different device.

One important detail is that behaviour can differ between development, TestFlight and live production. Expo builds, local simulator builds and TestFlight distributions do not always behave identically, especially around timing and network behaviour. I found it useful to keep a small checklist and run through the full subscription journey several times on a physical device before each submission.

AdMob test and production behaviour

Google AdMob added another layer. During development I used test ad unit identifiers. This is important because using live ad units too early can lead to policy issues and unexpected behaviour. At the same time, if you forget to switch to production identifiers after approval, you will never see real adverts.

The pattern that worked for SpellTales was:

  • Use AdMob test IDs throughout local and early TestFlight testing.
  • Confirm that the layout and frequency of ads made sense in the context of the app.
  • Switch to production IDs only once the app was accepted and ready to be released.

Re submission and final approval

After addressing the issues Apple raised, clarifying data handling, fixing the demo account, adjusting the registration flow and updating the promotional material, I submitted SpellTales again.

This time the review process completed without further questions and the app was approved for sale. There was nothing dramatic about that final step. It was simply the result of aligning the app with the existing guidelines and being clearer in how I explained what the app did and how it treated data.

Key lessons from the submission journey

Looking back over the submission process, a few points stand out.

  • Take the metadata in App Store Connect seriously. It is part of the product, not an afterthought.
  • Understand the difference between marketing versions and build numbers early, and make sure Info.plist is not quietly overriding your choices.
  • Expect Expo, Xcode and TestFlight to behave slightly differently, and test in all of them.
  • Treat demo accounts as part of your release process, and test them before every submission.
  • Be ready to explain analytics, advertising and subscription behaviour clearly, especially in apps used by children.
  • Think carefully about when registration is required, and make it optional unless there is a strong reason to do otherwise.

For me, the App Store submission process turned SpellTales from a project that ran on my own device into a product that could be understood and trusted by other people. It required more effort than I expected, but the process itself was not arbitrary. It simply forced me to articulate the details I had previously left implied.

Loading

Related Post