Build an iOS App with a revenue 1,338$ / mo

A full tutorial will walk with you step by step

Monthly Revenue

I have worked on this app for a long time, which gets me revenue monthly ~ 1300$. I wanted to share my experience with developers so they use their skills to earn more.

Revenue from Mars 1 until Mar 7 excluding Ads revenue.

You can see here the total revenue

Before we begin

This tutorial is designed for software developers who like to learn advanced swift. This tutorial will give you enough understanding of swift and how to build an APP from scratch. I would love to help developers to build their own apps with clean code way. While most of the tutorials are not a really good reference for building an app, you are going to see here a full tutorial on how to start building an application following an MVVM architecture and integrating in-app purchase.

Before proceeding with this tutorial, you should have a basic understanding of Computer Programming terminologies and a knowledge of programming language.

So, if you are a beginner and you don’t know too much about swift, it is ok you can follow, but the best is to get more in swift first, and then you start this tutorial because we will not gonna get in basic swift details.

Application Architecture

When we develop software, it is important not only to use design patterns but also architectural patterns. There are many different architectural patterns in software engineering. In Mobile software engineering, the most widely used are MVVM, Clean architecture, and Redux patterns.

Architecture concepts used here


  • Xcode Version 11.2.1+ Swift 5.0+


VPN stands for a virtual private network. We gonna see how to establish a VPN server in the upcoming articles. To build our app we are to define what are the VPN protocols we can use on iOS. Based on apple documentation VPN application can support IKEv2 and IPsec protocols for personal VPN. I suggest you read a bit about the network extension and what are the capabilities.

Back in the time when I started coding this app, I wanted to target both platforms, Android and iOS. Thus, I wanted to use a VPN protocol that can work on both and after long days of searching and trying, I found out that the IKEv2 is the best to use.

To understand what IKEv2 is you can check here.

Let’s Start Coding 🎉 💻

Oops.. wait! before we start coding, let’s talk a little bit about what we want in our app! For the sake of simplicity, we gonna work on 3 scenes and will use storyboards 🤒

  • OnboardigViewController
  • DashboardViewController
  • InAppPurchaseProViewController


You can create a new project and name it anything you want, but, in this tutorial, you will see 2 names VPN Guard and Secure VPN. The VPN Guard is based on Secure VPN which we’re going to use as a reference here.

The best way to keep our project clear is by organizing the folders, for that let us create them.

After grouping all the layers we have: Domain, Presentation, and Data Layers.

The domain layer is totally isolated, the innermost part of the onion. This layer can be reused in other projects.

The presentation layer contains UI such as view controller and view model, xib, and SwiftUI views, etc … Views are coordinated by ViewModels which execute one or many use cases. The presentation layer depends only on the Domain layer.

The data layer contains repository implementation and one or many data sources. The repositories are responsible for coordinating data from different data sources which can be local or external. Like the presentation layer, it depends only on the domain layer, also can have mapping decoding logic.

If you would like to know more about architectural patterns with iOS, you can leave me a comment here and I will post another story about architectural patterns with more details.


We can now start building the dashboard design, we will have a button in the middle. Once the user taps on it, we will establish a connection to the VPN server.

UILabel will show the connection status

UIImage/UILabel on the top is to show if the user is free or pro

In the middle, we can show the flag icon with the country name of the VPN server. On top of those, we will have a UIButton to trigger the VPN server list that we fetch from firebase.

UILabel showing the current public IP.

To instantiate viewController linked with storyboard from AppDIContainer we will create a protocol StoryboardInstantiable

The instantiateViewController will check the fileName which should be the same name for the storyboard `DashboardViewController.swift` `DashboardViewController.storyboard` and will instatiateViewController from that storyboard and return it.

So, our DashboardViewController will conform to the StoryboardInstantiable protocol, and will define a class func that will return the DashboardViewController

final class func create(viewModel: DashboardViewModel) -> DashboardViewController {let view = DashboardViewController.instantiateViewController()
return view

We will use this function to assign our ViewModel.

After connecting the connection button with the viewController as an action outlet inside its closure, we set the following code line:


This will, of course, trigger an error claiming that there is nothing such as ViewModel. Don’t worry, our goal is to create the ViewModel for DashboardViewController , so inside the viewController, we will define

private(set) var viewModel: DashboardViewModel!

and this will create for us the file DashboardViewModel . The ViewModel, as we said earlier, will have one or many use cases, so we need to think about that the view model's responsibility to trigger the connection / disconnecting to the server.

for the DashboardViewModel we define the input/output

By doing this, you will have a full idea of what this viewModel will do and give you the chance to add new functionality in the future if you want. The current VM’s logic is complicated, so you can create a new one and the functionality will not change.

DashboardViewModelRoute is an enum which you can guess from its name contains the routes.

The DashboardViewModelLoading is an enum usedto inform the View of the current status when we are connecting to the server, or fetching data from the API.

You can have a sneak peek at the viewModel here

Use Cases

Now we have our dashboardViewController and its ViewModel ready, it’s time to start with the use cases. We add our use cases in the domain layer, so we create NetworkVPNUseCase

So the main use cases for VPN are: connect, disconnect, load configurations, get status, and if we are using API to load the server we can add fetch servers.

Define a protocol that contains all the cases that we want like this:

We will create a final class DefaultNetworkVPNUseCase that conforms to NetworkVPNUseCase . We are still missing the repository that will be injected into the Data layer. This repository will have the same cases of the NetworkVPNUseCase . We will thus add it to the domain

path: Domain/interfaces/Repositories/

Back to the DefaultNetworkVPNUseCase we declare a private property of the repository

private(set) var vpnManager: DVPNRepository

you can find here the final look of the use cases class.

As you know, the domain layer is isolated, and we don’t use any third party inside. Therefore, I have created a special enum to handle the VPN status NetworkVPNStatus which we will map it from NEVPNStatus .

Now after we completed NetworkVPNUseCase , we can go back to the DashboardViewModel and inject the use cases in. We will get back to that soon.


Inside the data layer, we will create VPNRepository and VPNManager which will take care of the all process of establishing a connection, load configurations, and disconnect.

1- IPSec/IKEv2 connection

First, you need to add new capabilities to your app. So, select your project to navigate to signin & capabilities

Click on capability on the top left and add Personal VPN

Add Keychain sharing

For the Keychain, we want to add a keychain group domain where you can add whatever you want.


Luckily Apple provides a nice set API for connecting to a VPN using IPsec/IKEv2 without any third-party library.

Let me explain how it works before we start coding. The DefaultVPNManager will do the following:

1- Load preferences

2- Change the preferences to a new set of values (username, password, host, etc…)

3- Save the preferences

4- Start the connection

It sounds weird that we load the preferences first although there might no preferences to load. But, this is how apple decided how things should be done. If in any case, you saved the preferences before you load them, the process will fail.

The password and shared key will be saved in the keychain, I am going to write an article about the keychain later, so you can leave a comment asking when it will be ready 🙂 or follow me on Twitter 🐦 (my account still new).

DefaultVPNManager will conform to VPNRepository which will be in the Data/VPNManager/Repository this protocol will hold properties and functions

The NEVPNManager is what we need here. It will give us the ability to create and manage VPN configurations, see apple documentation here

To have an instance of the NEVPNManager I have created an extension of the NEVPNManager protocol which will ease our life while coding.

Whenever we call the manager we will have a reference of NEVPNManager as computed value. See here for more details about properties in swift.

the status will return the current status of the VPN, and that is the reason why we created a new enum so we don’t use the NetworkExtension inside the domain layer.

func registerNotification() will notify us when the status of the VPN change. So the reason behind using NotificationCenter here is that when vpnManager.connection.startVPNTunnel() succeeds, this doesn’t mean that we have established a connection successfully but that the process of establishing a VPN tunnel has been started successfully. Therefore, I had to have a walk around to get the real status. You will see with me how it works in few moments.

Back to the DefaultVPNManager you can check it here. You will notice that there is compile conditions like #if targetEnvironment(simulator) Oh well, bad news the VPN doesn’t work on the simulator, so to avoid any crash while debugging the UI, I added it.

Also, note that the manager.loadFromPreferences and manager.saveToPreferences callbacks are asynchronous

When we save the configuration, we need first to define the protocol that we’re using. In my case, the application can accept both IPSec and IKEv2. So by checking the protocol type of the server here, we’re trying to connect and configure it based on the type.

For IPSec:

case .IPSec:let p = NEVPNProtocolIPSec()p.useExtendedAuthentication = truep.localIdentifier = account.localID ?? "VPN"p.remoteIdentifier = account.remoteIDif account.pskEnabled {p.authenticationMethod = .sharedSecretp.sharedSecretReference = account.getPSKRef()} else {p.authenticationMethod = .none}pt = p

For IKEv2:

case .IKEv2:let p = NEVPNProtocolIKEv2()p.useExtendedAuthentication = truep.localIdentifier = account.localIDp.remoteIdentifier = account.remoteIDif (account.pskEnabled) {p.authenticationMethod = .sharedSecretp.sharedSecretReference = account.getPSKRef()p.passwordReference = account.getPasswordRef()} else {p.authenticationMethod = .none}p.deadPeerDetectionRate = .mediumpt = p

for configOnDemand, we’re not gonna configure it as it’s not the case for now.

Now that we are done configuring the personal VPN, it’s time to inject the domain layer repository into the data layer. To do so, we will createDefaultVPNRepository final class inside /Data/Repository

We will make the DefaultVPNRepository conform to DVPNRepository which will provide us the ability to use it. you can check the full code here.

Now we are ready to complete setup the DashboardViewModel .

DashboardViewModel Part 2:

inside the ViewModel we set as private property:

private var networkVPNUseCase: NetworkVPNUseCaseinit(networkVPNUseCase: NetworkVPNUseCase) {

we register now the observer to get the VPN status that we already set it up in the VPNManager

NotificationCenter.default.addObserver(self, selector: #selector(statusDidChange(_:)), name: NSNotification.Name.NEVPNStatusDidChange, object: nil)

The statusDidChange will update the label if the status did change

On viewDidLoad, we will notify the ViewModel so that it loads the configurations. This is how it will gonna look like on the DefaultDashboardViewModel side:

func viewDidLoad() {
self.networkVPNUseCase.loadVPNConfig {}

When the button connect on the DashboardViewController trigger up we gonna tell the ViewModel that we need to establish a connection if the status currently is disconnected, and the same if the current status is connected, we should disconnect.

func didConnect() {
networkVPNUseCase.connect(configuration: vpnAccount.value)
func didDisconnect() {

Where will this logic be handled? as MVVM the view should do any kind of logic, it only notifies the ViewModel with the current status/action and the VM will perform the proper action, and here’s how we gonna do this logic:

func connectDisconnect() {
if status.value == .connected || status.value == .connecting {
} else if (status.value == .disconnected || status.value == .invalid)) {
self.networkVPNUseCase.connect(configuration: vpnAccount.value) }

so basically we check the status we got from the observing NSNotification.Name.NEVPNStatusDidChange if it is connected we disconnect and vice versa.

Now the only left in this part is to bind the ViewModel

so inside the ViewController on viewDidLoad we perform bind(viewModel)

where bind() is a private function that sets the observer’s keys.

private func bind(_ viewModel: DashboardViewModel) {viewModel.loadingType.observe(on: self, observerBlock: { [weak self] in self?.handleLoading($0)})viewModel.status.observe(on: self, observerBlock: { [weak self] in self?.handleConnectionStatus($0)})viewModel.route.observe(on: self, observerBlock: { [weak self] in self?.handleRouting($0)})viewModel.premiumStatus.observe(on: self, observerBlock: { [weak self] in self?.handlePurchaseStatus($0)})viewModel.vpnAccount.observe(on: self, observerBlock: {[weak self] in self?.handleVPNSelection($0)})viewModel.currentIP.observe(on: self, observerBlock: {[weak self] in self?.handleIPUpdates($0)})viewModel.loadRequestAd.observe(on: self, observerBlock: {[weak self] in if $0 { self?.interstitial.load(GADRequest())}})}

You can use RxSwift library for rx instead of the Observable

The next Part Coming soon

✌️If you like me to continue this tutorial please follow me here and on Twitter, I will be available for any question and will do my best to answer ❤️

👉 [Part 2]

Software Developer, Tech enthusiastic, iOS #swift \\ Android #kotlin

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store