How to Publish Your iOS App on the App Store

Publishing an iOS app on Apple’s App Store is how you make your app readily available to anyone with an iOS device (and Apple Silicon device!) that wants to use it. Your app must pass a review by a human Apple employee, as well as a number of automated checks to ensure compliance with their policies.

Let’s jump right in!

Apple Developer Program

Apple Developer Program portal

In order to publish your app on the App Store, you need to be a part of the Apple Developer Program which costs $99 per year (as of 2022). You can enroll here. Keep in mind before you enroll, your app needs to be ready. Apple doesn’t allow apps in “beta” status, so if you’re still testing, make sure you finish your app before doing this.

Once you’re enrolled, it’s time to head over to App Store Connect!

App Store Connect

App Store Connect Portal

App Store Connect is, as it’s name suggests, where developers connect their apps to the App Store. You can find it here. To upload an app, click on the “My Apps” button.

On the next page, we’ll click on the “plus” button to add an app, then click on “New App”.

That will pop a window up that asks you to fill out some basic information about your app:

If you’ve come this far with an app, you probably already know what should go here, with the exception of SKU, which you can just make up. Click create, and the app is created.

Version Information

One the first page, you need to fill out a bunch of info for this version of your app. Since you can upload an unlimited number of versions in the future, each version submitted has its own information fields:

Version Info Page

The first step is to take some screenshots on various size iPhones running your app. If your app runs on the simulator, you can just use that.

There are a number of other fields, such as “Promotional text”, “Description”, “Keywords”, “Support URL”, and some others which should all be pretty self-explanatory. There’s a little question mark button by each one to help. The most important thing on this page, though, is the build:

You should be able to find the builds you have uploaded to App Store Connect under the “TestFlight” tab up at the top. How do you upload a build? There are a couple different ways, but I did it through Xcode. From your project in Xcode, go to “Product” –> “Archive”. It will first go through an archive process then bring you to this screen:

Clicking through “Distribute App” and a couple more screens will run though some checks and upload to App Store Connect (not the App Store!). For this to work you need to have registered your valid Apple Developer Program account under Xcode –> Preferences –> Accounts. Also if this your second or more times uploading, make sure to increment the build number under Your Project –> General –> Build. The upload process looks like this:

Once this is done, you’ll need to go back to “TestFlight” and you’ll see the build status is “Missing Compliance”:

Click on “Manage” and answer some questions about encryption, and you should be good to go. Back on the “App Store” tab, go down to the build section and select your newly uploaded build.

App Information

The app information section (accessible via “App Information” on the right) requires surprisingly little information about your app. Fill it out and you’re done:

Pricing and Availability

The pricing and availability section just asks if you want to charge for your app, and in what countries it should be available. This particular app is free, and I made it available everywhere:

App Privacy

The app privacy tab requires that you have a website posting your app’s privacy policy. I made very simple one and published on my blog.

Submit

The rest of the tabs are just areas where you can check on your app – ratings, version history and such.

Go back the the version page, and click “Submit”, and your app will be submitted for review!

It’s quite possible that the version you submitted could be rejected for one reason or another. You can communicate with the reviewer via the “App Review” tab on the left (it shows up when there’s feedback from Apple). Be sure to come back and answer their questions, concerns or requests.

Good luck!

iOS Swift – Building a Times Tables App – Part 1

Previously, we built an Android times tables practice app in Kotlin, as well as a backend-only web times tables app in Python. Today we’re going to port this app to Swift so it can work on iOS for an iPhone as well. Let’s get started! You can find parts 2 and 3 of this series here:

iOS Times Tables App – Part 2
iOS Times Tables App – Part 3

At the time of writing, this was done in Xcode 12.5.1 and Swift 5.4.2. All the code for this app can be found on my github account:

https://github.com/jamesmcclay/swift_times_tables

Create a project in Xcode

We’re going to create an iOS app in Xcode, so make sure you select “iOS” in the Xcode new project window:

Xcode New Project Window

There are, in fact, many different ways to write an app in Xcode/iOS/swift. As a matter of complete personal preference, I like doing everything “programmatically”. In iOS development you have two choices: use Interface Builder or programmatically. Interface Builder uses the story board to create UI objects/elements, while programmatic creates the same elements from swift code without using Interface Builder. I happen to prefer the latter.

The only thing that is really difficult to create from code is a UIViewController, so I typically add those from the storyboard. Since Apple gives us the first UIViewController, we’ll jump right into creating the UI elements.

Create the UI objects

We’ll first create global variables to hold our UI elements so we can access them from anywhere in the class, like this:

let mainLabel:UILabel
let subLabel:UILabel
let tableInput:UITextField
let multiplierInput:UITextField
let normalMode: UISwitch
let normalLabel: UILabel
let randomMode: UISwitch
let randomLabel: UILabel
let startButton: UIButton
var modes: [UISwitch] = []

I’ll explain what each one does:

  • mainLabel: a title-like piece of text to show the name of the app.
  • subLabel: some text that tells the user to enter a table or leave it blank to practice all tables.
  • tableInput lets the user select a specific table to practice.
  • multiplierInput lets the user specify how far up they want to practice. The default is to practice to 12, but this input allows it to be configured.
  • normalMode: a switch for “normal mode” which practices in sequence.
  • normalLabel: a label attached to the switch saying “normal mode”.
  • randomMode: a switch for “random mode” which practices randomly.
  • randomLabel: a label attached to the switch saying “random mode”.
  • startButton: the button to begin practicing.
  • modes: an array to hold the two switches, more on that in the next post.

Now that we have these created, we’ll add a couple of functions that allow us to create objects to put in these variables, without ever touching Interface Builder. They look like this:

convenience init() {
    self.init()
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}

I won’t go into how these work, as it’s a bit distracting from the app building. Suffice to say they let us create UI objects programmatically.

Our objects can be created in the required init? function before the line with super.init is run. The way I typically create objects is using closures, which is just a function without a name. You can assign whatever the closure returns to a variable. So I just create a bunch of closures that create the objects and assign them to the global variables. This next bit is long, but it’s just a bunch of boilerplate creating these elements/objects the way that I wanted them. The required init? function now looks like this:

required init?(coder aDecoder: NSCoder) {
    mainLabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = "James McClay's Times Tables!"
        label.numberOfLines = 2
        label.textAlignment = .center
        label.font = UIFont.preferredFont(forTextStyle: .largeTitle)
        label.adjustsFontForContentSizeCategory = true
        return label
    }()
    subLabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = "Enter a table to practice,\n or blank for all:"
        label.numberOfLines = 2
        label.textAlignment = .center
        label.font = UIFont.preferredFont(forTextStyle: .title2)
        label.adjustsFontForContentSizeCategory = true
        return label
    }()
    tableInput = {
        let input = UITextField()
        input.translatesAutoresizingMaskIntoConstraints = false
        input.placeholder = "All Tables"
        input.borderStyle = .bezel
        input.keyboardType = .numberPad
        return input
    }()
    multiplierInput = {
        let input = UITextField()
        input.translatesAutoresizingMaskIntoConstraints = false
        input.placeholder = "Max Multiplier (12)"
        input.borderStyle = .bezel
        input.keyboardType = .numberPad
        return input
    }()
    normalMode = {
        let mode = UISwitch()
        mode.translatesAutoresizingMaskIntoConstraints = false
        mode.tag = 0
        return mode
    }()
    normalLabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = "Normal Mode"
        label.font = UIFont.preferredFont(forTextStyle: .body)
        label.adjustsFontForContentSizeCategory = true
        return label
    }()
    randomMode = {
        let mode = UISwitch()
        mode.translatesAutoresizingMaskIntoConstraints = false
        mode.tag = 1
        return mode
    }()
    randomLabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = "Random Mode"
        label.font = UIFont.preferredFont(forTextStyle: .body)
        label.adjustsFontForContentSizeCategory = true
        return label
    }()
    startButton = {
        let button = UIButton(type: .system) as UIButton
        button.translatesAutoresizingMaskIntoConstraints = false
        button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .title2)
        button.titleLabel?.adjustsFontForContentSizeCategory = true
        button.setTitle("Start!", for: .normal)
        return button
    }()
    super.init(coder: aDecoder)
    modes = [normalMode, randomMode]
}

modes = [normalMode, randomMode] is put after super.init because that’s where self is created. I’ll use that array to make sure only “normal” or “random” mode can be selected, both.

Now that we have all those elements created, let’s lay them out on the view.

Using autolayout to put objects on the view

All of the elements created above have translatesAutoresizingMaskIntoConstraints set to false, which means that Autolayout is turned on. Autolayout basically allows a swift developer to put UI elements on the view and have their position dynamically calculated based on the view size and their relative position to other elements.

The way I like to do this is to write a function to hold the code for each logical area, and call each function from viewDidLoad. So my viewDidLoad looks like this:

override func viewDidLoad() {
    super.viewDidLoad()
    self.navigationController!.navigationBar.isHidden = true
    setupLabels()
    setupInputs()
    setupModes()
    setupStartButton()
}

First we’ll set up the “labels”, the main one and the sub one. My setupLabels function looks like this:

func setupLabels() {
    view.addSubview(mainLabel)
    view.addSubview(subLabel)

    mainLabel.topAnchor.constraint(
        equalTo: view.safeAreaLayoutGuide.topAnchor,
        constant: view.frame.height * 0.1
    ).isActive = true
    mainLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    mainLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8).isActive = true

    subLabel.topAnchor.constraint(
        equalTo: mainLabel.bottomAnchor,
        constant: view.frame.height * 0.04
    ).isActive = true
    subLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    subLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.7).isActive = true
}

I really hate how complicated this looks, because it’s not complicated. The autolayout code here is doing just a few basic things:

  • Add the UI element to the view as a subview (it puts the element on the screen).
  • Anchor its y-axis, usually by putting a constraint on the topAnchor. I usually give a constant with that one as well to provide some dynamically-created spacing, with view.frame.height * 0.1.
  • Anchor its x-axis, a lot of times it can just be constrained to the view.centerAnchor.
  • Anchor the width, usually to a percentage of the main view.

Once those constraints are in place, the element will properly show on the screen. That’s really all that’s happening here. If you have any questions, please comment or contact me!

Now let’s create setupInputs:

func setupInputs() {
    view.addSubview(tableInput)
    view.addSubview(multiplierInput)

    tableInput.topAnchor.constraint(
        equalTo: subLabel.bottomAnchor,
        constant: view.frame.height * 0.04
    ).isActive = true
    tableInput.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    tableInput.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.4).isActive = true

    multiplierInput.topAnchor.constraint(
        equalTo: tableInput.bottomAnchor,
        constant: view.frame.height * 0.02
    ).isActive = true
    multiplierInput.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    multiplierInput.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.4).isActive = true
}

You might have noticed this looks very much like setupLabels. That’s because autolayout requires a great deal of boilerplate but necessary code. I haven’t found a good way to reduce the plates on my boiler for this kind of code, so I end up with a fair amount of it when laying things out programmatically.

Now let’s create setupModes:

func setupModes() {
    view.addSubview(normalMode)
    view.addSubview(normalLabel)
    view.addSubview(randomMode)
    view.addSubview(randomLabel)
    
    normalMode.topAnchor.constraint(
        equalTo: multiplierInput.bottomAnchor,
        constant: view.frame.height * 0.04
    ).isActive = true
    normalMode.leadingAnchor.constraint(equalTo: tableInput.leadingAnchor).isActive = true
    normalMode.addTarget(self, action: #selector(modeTapped), for: .touchUpInside)
    
    normalLabel.leadingAnchor.constraint(
        equalTo: normalMode.trailingAnchor,
        constant: view.frame.width * 0.01
    ).isActive = true
    normalLabel.centerYAnchor.constraint(equalTo: normalMode.centerYAnchor).isActive = true
    normalLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.3).isActive = true
    
    randomMode.topAnchor.constraint(
        equalTo: normalMode.bottomAnchor,
        constant: view.frame.height * 0.02
    ).isActive = true
    randomMode.leadingAnchor.constraint(equalTo: tableInput.leadingAnchor).isActive = true
    randomMode.addTarget(self, action: #selector(modeTapped), for: .touchUpInside)
    
    randomLabel.leadingAnchor.constraint(
        equalTo: randomMode.trailingAnchor,
        constant: view.frame.width * 0.01
    ).isActive = true
    randomLabel.centerYAnchor.constraint(equalTo: randomMode.centerYAnchor).isActive = true
    randomLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.3).isActive = true
}

Once again, very similar to the previous ones with one difference. We’re attaching a label to the right edge of each switch. So normalMode has a corresponding normalLabel to explain to the user what the switch is for. The normalLabel.leadingAnchor is constrained to the normalMode.trailingAnchor with some spacing given to constant.

Finally, we’ll set up the start button with setupStartButton:

func setupStartButton() {
    view.addSubview(startButton)

    startButton.topAnchor.constraint(
        equalTo: randomMode.bottomAnchor,
        constant: view.frame.height * 0.04
    ).isActive = true
    startButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    startButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.3).isActive = true
}

Try it out!

Now that we have all the UI elements created and laid out and constrained, they should fall into place! On the simulator it’s looking good:

The app running on Xcode iPhone Simulator

In the next post, we’ll add some logic so our elements actually do things.

See you next time!