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
- Clean Architecture https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
- Advanced iOS App Architecture https://www.raywenderlich.com/8477-introducing-advanced-ios-app-architecture
- MVVM
- Data Binding
- Dependency Injection
- SwiftUI and UIKit view implementations by reusing the same ViewModel (at least Xcode 11 required)
Requirements
- Xcode Version 11.2.1+ Swift 5.0+
VPN
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
Coding
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.
DashboardViewController
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:
viewModel.connectDisconnect()
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.
Data
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.
Setup
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.observeStatus()
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() {
self.networkVPNUseCase.disconnect()
}
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 {
self.networkVPNUseCase.disconnect()
} 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