Learn how to build iOS apps
Get started with iOS 12 and Swift 4
Sign up for our iOS development course Zero to App Store and learn how to build professional iOS 12 apps with Swift 4 and Xcode 10.
In this tutorial we are going to build a blockchain app. Blockchain technology is hot right now, and it has an immense amount of potential. But did you know that the future of the blockchain is building apps and services on top of the blockchain?
I’ll show you how in this tutorial. You’ll learn how the blockchain works, what proof of work is for, and I’ll show you step-by-step how to build an app that interacts with the blockchain.
The blockchain app we are going to build together is called Habitat, and you can use it to purchase fictive habitats on Mars.
Always wanted to put your mark on the Schiaparelli crater? Now you can. And best of all: your purchase is immortalized in the blockchain! Yes, it’ll stay there forever. I’ll show you what that means in this tutorial.
We’re not going to build the blockchain itself. There are plenty of smart people doing that!
We’ll use a webservice to connect to a Blockchain-as-a-Service, called Tierion. Why? So we can build on top of the blockchain, because that’s where the real value of the blockchain technology lies.
After completing this iOS tutorial, I want you to be able to build your own blockchain app – for whatever purpose – on top of blockchain technology. Let me know what you come up with!
Ready? Let’s go.
Do you want to build your own iOS apps, or become a professional iOS developer? Check out Zero to App Store, our flagship iOS development course that shows you how to bring your iOS 11 app projects to life with Swift 4. See you inside? » Learn more
Let’s start by finding out what a blockchain really is. It’s awesome, important, and not that complicated.
Ledgers
A blockchain is a so-called “ledger” of facts. And a ledger is like a database, with rows and columns.
You’ve probably used ledgers all your life without knowing it. Your iPhone contacts list is a ledger, and so is your back-off-the-napkin list of vacation expenditures, and so is last week’s shopping list.
The ledger is decentralized in a blockchain, which means that it does not have a central point of storage. Compare this with the opposite: a bank that has saved your money or valuables in one central vault. When the bank gets robbed, your money is gone.
The blockchain is stored across multiple computers, via a peer-to-peer network, replicated on each one of them. Members of this network are called nodes. Every node has a copy of the blockchain. Blockchain nodes use cryptography to securely communicate with each other, and verify the identities of sender and receiver.
The big problem that blockchain solves is so-called consensus. Let’s look at an example.
I know about ledgers mostly from a video game called L.A. Noire, published by Rockstar Games. In the game, that’s set in Los Angeles in 1947, you’re a police detective and you have to solve murders.
One way to gather evidence in your investigation is by checking out ledgers in shops, for instance for the sale of guns. In one murder case, you can find out that Bob bought a gun similar to the murder weapon in a gun shop close to the murder location. That’s a pretty helpful ledger!
Which Ledger Entry Is Right?
But how do you know that the ledger contains the right information? That’s a big question!
You’d typically solve that by means of authority and trust. You trust that the shopkeeper puts the correct entries in his ledger. You can also verify entries in the ledger with other independent data sources.
A few examples:
In most of these examples you rely on the trust, integrity and authority of an intermediary. When your trust in this intermediary fails, all hell breaks loose. When you can’t trust the independent intermediary anymore, who can you trust?
The blockchain now has a problem. Because it’s decentralized, there is no central source you can trust. How can you trust any of its nodes? How do you add correct entries to the ledger? What’s keeping a node from sending false information for its own benefit?
Proof of Work
The blockchain uses a principle called proof-of-work. Before a fact is accepted as true, you need to prove that you did some work.
Here’s how it works:
Every block contains a reference to the previous block. Because of the way hashing works, you can’t change a block once it has been confirmed. So you can’t easily go back in time and change a block earlier in the chain.
The blocks of popular cryptocurrencies, like Bitcoin and Ethereum, contain monetary transactions. When a transaction is confirmed, you can ensure that the money can only get spent once.
Miners get a reward when they successfully mine a block. Such a reward, like bitcoin tokens, is typically worth a lot of money. That’s why they keep mining!
Thanks to the proof-of-work principle and cryptographically secure hashing algorithms, the blockchain is secure. You can only spend your bitcoin once. But how does that work for blockchains that aren’t used for monetary purposes?
The explanations in this chapter are deliberately kept simple. I’m sure I made mistakes somewhere. You need a PhD in cryptocurrency if you want to understand 100% how this stuff works. Did you spot a mistake? Feel free help us understand blockchain technology better by leaving a comment below!
Sign up for our iOS development course Zero to App Store and learn how to build professional iOS 12 apps with Swift 4 and Xcode 10.
The Bitcoin blockchain works because bitcoin are worth money. Miners mine bitcoin, and confirm monetary transactions, because they’re incentivized to do so. As a result, we can pay for things with bitcoin.
If you want to build an app on top of blockchain technology, to provide proof of some facts, like housing prices, medical records or education certificates, then do you need miners?
Yes.
Think about it like this. Confirming monetary transactions is just a service that miners provide. Anything that provides a proof of facts is a service too.
Just as a service-providing developer, writer, or designer charges money for their time and creativity, the facilitator of a blockchain can charge for the proof of facts that their blockchain provides.
An organisation can pay miners with the money it makes from providing the blockchain service. It can do so with cryptocoins, or by using other means. Reading data from the blockchain is free, but writing to it costs money. It’s an intriguing system, isn’t it?
Blockchain-as-a-service is capitalism at its finest. You need the proof, and we’ve got the miners who can provide it.
Some permissioned blockchains don’t use proof of work, but instead rely on other ways to verify facts. These blockchains don’t need miners, and as a result, some consider them shared ledgers instead of blockchains.
A great number of companies offer Blockchain-as-a-Service platforms, including the typical giants like Microsoft, IBM, Amazon, SAP and Oracle. Interesting projects include Hyperledger, from the Linux Foundation, and R3, that’s backed by a ton of big banks.
In this tutorial we’re going to use the public Blockchain-as-a-Service, or blockchain proof engine, Tierion. Tierion has a simple and powerful API, and we are going to use it to store data in the blockchain, read from it, and verify that data.
The Tierion platform has an added benefit: they’re using the Chainpoint open standard to link their own blockchain to the larger public Bitcoin and/or Ethereum blockchains. It’s like a chaining of blockchains by using a so-called Merkle tree, that ultimately leads to a chain of proof that’s linked to the larger blockchains.
What that means in layman’s terms is that, when using a Blockchain-as-a-Service such as Tierion’s, you’re not just relying on the integrity of their organization and platform, but also using the decentralized power of the Bitcoin and Ethereum blockchains. It’s another level of chain-of-proofs and security.
Important: In this iOS tutorial we’ll directly interact with the public Tierion API, for educational purposes. You’d never do that in a production app! I’ll explain more going forward, but if you are building a production-ready blockchain app, you’ll want to use a go-between webservice for authentication, logging and access control, depending on the requirements of your app.
Now, back to the blockchain.
Blockchain technology is as useful as the purpose for which you use it. Although it is compelling to become a facilitator of blockchain technology, I think the real innovations will be made by small companies that build on top of the blockchain. That’s you, by the way!
Some ideas for blockchain apps:
When implemented correctly, blockchain technology allows you to verify the truth.
I think that the current state of blockchain technology is only 1% of its full potential. It’s insane! (On a side note, just because it says “blockchain” doesn’t mean it’s 10x better than the solution we use now. Means vs. ends!)
Alright, enough about blockchain technology. Let’s use it to build a blockchain app!
This tutorial wouldn’t be much fun if it didn’t show you how to create a blockchain app. So that’s what we’re going to do next!
The blockchain app we’ll build is called Habitat. Here’s how it works:
Think of it like a land registry or cadastre, that stores proof that Bob is the truthful owner of Bob’s Red Fantasy Land in the Schiaparelli crater on Mars.
When a user taps one of the cells, they can choose to Purchase or Verify a habitat. Any habitat will show its most recent owner. You can also track the blockchain status of a purchase with the app.
We’ll use two datastructures in our app:
let habitats = [
["Schiaparelli", 35000],
["Huygens", 50000],
["Copernicus", 250000]
]
var purchase = Purchase()
purchase.habitat = "Schiaparelli"
purchase.owner = "Elon Musk"
purchase.date = "March 14, 2018"
purchase.recordID = "IAgIOru770WwipjxJXj1zQ"
Note: It’s only logical that we use Mars’ unofficial currency for habitat prices: the Marsoleon, with symbol M.
We’ll use a number of tools and frameworks to build our app:
This tutorial assumes you have at least a basic understanding of Swift and Xcode, and that you’re familiar with CocoaPods, networking, JSON and REST APIs. If you feel out of your depth, I recommend you check out our Zero to App Store iOS development course.
Sign up for our iOS development course Zero to App Store and learn how to build professional iOS 12 apps with Swift 4 and Xcode 10.
We are going to start with setting up the blockchain app project in Xcode. The steps are roughly as follows:
Let’s go!
1. Create The Project In Xcode
Creating the project in Xcode is as easy 1-2-3. Here’s how:
File -> New -> Project...
HabitatApp
, set your Organization Name and Identifier and click Next2. Make A Podfile And Install Pods With CocoaPods
Do this to set up the pods for this project:
HabitatApp
project in the Project Navigator on the left and choose New File...
.Podfile
(no extension, just “Podfile” with an uppercase “P”) and make sure to save it alongside the HabitatApp.xcodeproj
file.source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '11.0'
use_frameworks!
target 'HabitatApp' do
pod 'Alamofire'
end
cd
and type this on the command-line: pod install
. This installs Alamofire into the Xcode project.HabitatApp.xcworkspace
workspace file in Xcode.3. Set Up A Tierion Account
Our blockchain app will use the Tierion Blockchain-as-a-Service (BaaS) platform. You’ll need a free account to use their API.
Here’s how you get one:
HabitatApp
(don’t forget this step)4. Get Tierion API Key And Datastore ID
Next, you’ll need to note two things:
Here’s how you get them:
HabitatApp
Datastore in the list on the leftOur blockchain app will use a simple table view to display information about the habitats and their owners.
Here’s what we’ll do next:
1. Delete Boilerplate View Controller
The Single View App template contains a boilerplate view controller. We don’t need it, so let’s delete it.
Here’s how:
ViewController.swift
file in the Project Navigator and delete it, by right-clicking and choosing Delete, and then Move to Trash.Main.storyboard
in Interface BuilderEasy-peasy!
2. Create A New Table View Controller Class
Next, let’s create that table view controller we need.
As you know, a table view controller is just a view controller with a table view as its view
property, and a few extra delegates.
Here’s what you need to do:
New File...
UITableViewController
in the Subclass of fieldHabitatTableVuewController
in the Class fieldAppDelegate.swift
file and make sure that HabitatApp
is checked for TargetsThe new table view controller class appears. Feel free to remove the code comments, so your code looks a little bit more like mine. Also, I usually put view controllers in their own folder in the Project Navigator.
For the remainder of this tutorial, next time when you’ll need to create a new file, you now know what to do. Right?
3. Set Up The Table View Controller In Storyboard
Awesome! We just removed the only view controller of our blockchain app from the app’s Storyboard. Let’s add another back in!
Here’s how:
Main.storyboard
in Interface Builder (click on it in the Project Navigator)HabitatTableViewController
in the Identity Inspector (important!)Editor -> Embed In -> Navigation Controller
That last step wraps the table view in a navigation controller. Even though we won’t use navigation in this blockchain app, it gives a nicer structure to the app’s user interface.
You can also set the navigation title to “Habitats”, by setting the Title field of the Navigation Item of the table view controller (with the Attributes Inspector).
When you run the app with Command-R
or the Play button at this point, the navigation bar and table view controller should show up nicely!
Our blockchain app will display information about habitats on Mars, including their owners and purchase prices.
The table view isn’t of much use to us right now, because it can’t display that data. Let’s change that!
We’re going to create a custom table view cell. That’s a UITableViewCell
subclass with a custom XIB file, that contains layout information.
Here’s what we’ll do:
UITableViewCell
subclasscommonInit()
functionset(name:owner:price:)
functionGet a move on!
1. Create The UITableViewCell Subclass
First, create a new file with the following settings:
HabitatCell
UITableViewCell
2. Set Up HabitatCell’s Properties
Then, we’re going to create 3 properties, one for each of the UI elements this table view cell has.
Add these 3 properties to the HabitatCell
class:
nameLabel
of type UILabel
ownerLabel
of type UILabel
priceLabel
of type UILabel
Also do this:
@IBOutlet
and weak
You’ll (hopefully) end up with something like this:
@IBOutlet weak var nameLabel:UILabel?
@IBOutlet weak var ownerLabel:UILabel?
@IBOutlet weak var priceLabel:UILabel?
@IBOutlet
attribute tells Interface Builder that these properties can be connected to a UI element, as an outlet. The weak
attribute ensures that an instance of HabitatCell
doesn’t retain references to these outlets when the cell gets deallocated, and helps us to avoid a strong-retain cycle. Lastly, the properties are optionals because outlets can be nil
during their life-time.
3. Code A “commonInit()” Function
You already know that table view controllers efficiently reuse their cells, so they won’t have to be constantly created anew.
Every table view cell has two functions, awakeFromNib()
and prepareForReuse()
, to help with cell reuse.
awakeFromNib()
function is calledprepareForReuse()
function is calledIn both functions, you’ll want to set the UI of the cell to a default value. This “resets” the cell and ensures that a reused cell won’t accidentally contain the UI of a previous iteration of that cell. (If it does, you forgot to implement those 2 functions…)
So, long story short, we’re going to use a function called commonInit()
to code that resetting once, and use it twice.
Add the following function to the HabitatCelll
class:
func commonInit()
{
nameLabel?.text = ""
ownerLabel?.text = ""
priceLabel?.text = ""
}
Easy-peasy, right? This function resets each of the labels to contain an empty string.
Next, call this new commonInit()
function in awakeFromNib()
. That function now looks like this:
override func awakeFromNib()
{
super.awakeFromNib()
commonInit()
}
Finally, create a new function prepareForReuse()
. This function needs to be overridden from the superclass with the override
keyword. You’ll also need to call the superclass implementation for prepareForReuse()
. And of course, you need to call commonInit()
.
That function now looks like this:
override func prepareForReuse()
{
super.prepareForReuse()
commonInit()
}
You now have 3 functions set up:
awakeFromNib()
prepareForReuse()
commonInit()
And the first two functions both call commonInit()
in their function bodies. Awesome!
Do you notice how I’m first explaining the steps we are going to take, and only later on give you the right Swift code for that step? I do that deliberately! It’s easy to fast-forward through this tutorial by just copying-and-pasting the source code. You learn nothing that way! Instead, pace yourself, complete the steps as I describe them, and check your work with the Swift code samples.
4. Code A “set(name:owner:price:)” Function
Later on in this tutorial, we’ll use the custom table view cell together in the table view controller.
The table view controller will load the cell, and based on the habitat data, we fill that cell up with the right information. Technically, we could access the properties of the cell directly – but that stinks.
Instead, we’ll create a set(name:owner:price:)
function. It’ll set the properties. Doing this in your own apps has a few benefits:
Let’s code it! Here are the rough steps:
name
of type String
, owner
of type String?
and price
of type Int
nameLabel
, ownerLabel
and priceLabel
You can add a few nice-to-haves:
owner
parameter will be nil
. Use it to show a “For sale!” or “Currently owned by: [owner]” text depending on the ownership status.NumberFormatter
and numberStyle = .decimal
to nicely format the habitat price with thousands separatorsHere’s the Swift code:
func set(name: String, owner: String?, price: Int)
{
nameLabel?.text = name
ownerLabel?.text = owner == nil ? "For sale!" : "Currently owned by: \(owner!)"
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
if let price = formatter.string(for: price) {
priceLabel?.text = "\(price) M"
}
}
See what happens there?
name
parameter to the text
property of the nameLabel
, effectively setting the text the label showsownerLabel
and the owner
parameter, using the ternary conditional operator a ? b : c
to distinguish between a sold habitat and one for saleNumberFormatter
to format price
as a decimal with thousands separators, and including that imaginary “M” currency symbolAt a later point, we can use the set(name:owner:price:)
function like this:
cell.set(name: "Schiaparelli", owner: "Mark Watney", price: 50000)
This will show that information in the table view cell. Neat!
In the above code sample we’re using string interpolation with "Owned by \(owner)"
to use a variable’s value in a string. We’re also using conditional binding with if let
because string(for:)
returns an optional value.
5. Configure Cell XIB In Interface Builder
So… we’ve written some Swift code, but we haven’t set up any user interface elements in Interface Builder. Oops!
Let’s change that. Here’s what we’ll change in HabitatCell.xib
:
First, change the cell size:
375
for Width and 60
for Height, in the View sectionEasy! Now, add the 3 labels like this:
17.0
points, black color14.0
points, dark gray color16.0
points, black colorA few notes:
0
16
points, and the top and bottom margins are about 10
pointsFinally, connect the UI elements with their outlets! Here’s how:
nameLabel
, ownerLabel
and priceLabel
in the list, with small dots on the right.Now that we have the blockchain app’s UI set up, let’s bring it to life! We’ll need to add some data to the table view.
First, open the HabitatTableViewController.swift
file and add the following property to the top of the class:
let habitats = [
["Antoniadi", 10000],
["Cassini", 25000],
["Copernicus", 35000],
["de Vaucouleurs", 56000],
["Dollfus", 99000],
["Greeley", 120000],
["Herschel", 250000],
["Huygens", 314150],
["Koval'sky", 370000],
["Newton", 450000],
["Schiaparelli", 510000],
["Tikhonravov", 997000]
]
Your class now looks like this:
class HabitatTableViewController: UITableViewController
{
let habitats = [
["Antoniadi", 10000],
["Cassini", 25000],
...
That habitats
property is a constant, with the let
keyword, so it can’t change once it has been set.
We’re not specifying a type, so the Swift compiler will infer that for us, but we know that the type of habitats
is [[Any]]
.
Surprised? We’ve created a nested array, so an array of arrays, with both String
and Int
objects in it. An array can only have one type, so it has to be Any
to accomodate both String
and Int
objects.
The data in the habitats
array represent the names of craters on Mars, such as Schiaparelli, and the imaginary costs (in Marsoleons) of purchasing the habitats located there.
Next up, we gotta implement the following things:
register(_:forCellReuseIdentifier:)
in viewDidLoad()
numberOfSections(in:)
tableView(_:numberOfRowsInSection:)
tableView(_:cellForRowAt:)
Those three functions are delegate functions, and they are part of how a table view controller works.
At certain times, our implementations of those functions are called. They are used to provide the table view controller with information, such as the number of cells the table view contains. We can use them to customize the table view and to respond to events.
This is called delegation. On top of that, UITableViewController
has default implementations of these delegate functions that we override.
1. Register The Custom Table View Cell
First, we have to tell the table view we want to use a custom cell. We’ll do so by providing the XIB and a reuse identifier.
Add this code to viewDidLoad()
:
tableView.register(UINib(nibName: "HabitatCell", bundle: nil), forCellReuseIdentifier: "habitatCell")
What happens there?
register(_:forCellReuseIdentifier:)
on tableView
UINib(nibName: "HabitatCell", bundle: nil)
, which is the Interface Builder “XIB” file we created earlier"habitatCell"
that’s used to identify this kind of cellAlways wondered what the difference between a XIB and a NIB is? Both are Interface Builder files, i.e. they contain UI information, but a XIB uses an XML format (for Xcode) and a NIB uses a binary format (for the app binary). Therefore, Swift functions that use these UI files consistently use the word “nib”.
Then, your table view controller implementation should already include stubs for these 2 functions:
numberOfSections(in:)
tableView(_:numberOfRowsInSection:)
Adjust the functions so that…
habitats
arrayYour code now looks like this:
override func numberOfSections(in tableView: UITableView) -> Int
{
return 1
}
And this:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return habitats.count
}
Every collection in Swift, such as arrays and dictionaries, that conform to the Collection
protocol, have a property count
that’s equal to the number of items in that collection.
Finally, we need to change one last function: tableView(_:cellForRowAt:)
. This function is used to provide table view cells to the table view.
tableView(_:cellForRowAt:)
is, along with a table view instance, a value of type IndexPath
, that contains the section and row index of the cell.UITableViewCell
, so that’s the cell we need to create and provide back to the table view controller.Think of a table view as a spreadsheet with rows. Rows can be grouped together in a section. You need to provide the cells for the table view with tableView(_:cellForRowAt:)
. Here’s how it works:
numberOfSections(in:)
and tableView(_:numberOfRowsInSection:)
.tableView(_:cellForRowAt:)
is called every time the table view controller needs a cell. The function gets the row and section indices for that cell via the indexPath
parameter.indexPath.row
value as the index of your data source collection.Can you confirm for yourself that in our blockchain app, the indexPath.section
is always 0
, and that the indexPath.row
runs from 0
to 11
(12 in total)? Great!
Fun fact: The table view controller doesn’t request all needed cells at once. It will call tableView(_:cellForRowAt:)
selectively, so only for cells that are inside the viewport.
Your implementation of tableView(_:cellForRowAt:)
needs to do this:
"habitatCell"
as!
that cell to HabitatCell
, or use conditional binding with as?
habitat
and price
from habitats
using the indexPath
set(name:owner:price:)
on cell
with the right argumentsAnd here’s what that looks like in Swift code:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "habitatCell", for: indexPath) as! HabitatCell
if let habitat = habitats[indexPath.row][0] as? String,
let price = habitats[indexPath.row][1] as? Int
{
cell.set(name: habitat, owner: nil, price: price)
}
return cell
}
This is what happens in tableView(_:cellForRowAt:)
:
"habitatCell"
identifier and the indexPath
. This is how the dequeue mechanism of a table view controller works. It has a cache of cell objects that can be reused, because that’s quicker than creating new objects every time a cell is needed. UITableViewCell
. We need it to be of type HabitatCell
, so we can use our set(...)
function, so we force-downcast the cell to HabitatCell
. The cell object doesn’t change, but we treat as if it has the HabitatCell
type.habitats
to get to the right data:
habitats
array by using indexPath.row
.0
and 1
for the first and second elements respectively to get to habitat
and price
. (Check this with the habitats
array! See how it works?)Any
to String
and Int
. We’re combining this with optional binding, so the values get neatly assigned to non-optional constants habitat
and price
.habitat
and price
as arguments, we call cell.set(...)
. This is the function we created earlier, that puts the values as text in the right UI elements. Note how we’re using nil
for owner
, because these habitats have no owner yet.cell
. Because the type of cell
is a subclass of UITableViewCell
we don’t have to cast it back.Awesome! At this point you should be able to run your blockchain app with Command-R
or the Play button, and see every habitat show up in the table view.
Oh yeah! We’re getting to the good stuff. Now you’re finally going to connect to the blockchain…
You’re going to use Tierion’s blockchain API for 3 things:
Especially verification is important, because you want to know if a record has successfully become part of the blockchain.
Here are the basic steps:
Purchase
class, to structure purchase dataAPI
class, for the API codegetAllRecords()
function to get the latest Records from the APIgetAllRecords()
getLastPurchase(of:)
function to get the current owner of any habitat1. Create A Purchase Class
The purchase data, as introduced earlier in this tutorial, needs some organizing. We could pass it around as a dictionary in our code, but that’s prone to errors, so let’s create a class.
Here’s an example:
var purchase = Purchase()
purchase.habitat = "Schiaparelli"
purchase.owner = "Elon Musk"
purchase.date = "March 14, 2018"
purchase.recordID = "IAgIOru770WwipjxJXj1zQ"
With an array of Purchase
objects we can find out who owns which habitat. We’ll also store this data in the blockchain, and populate our Purchase
objects with data from the blockchain.
First, create a new file in Xcode. Use the Swift File template and name it Purchase.swift
.
Here’s the gist of that class:
Purchase
and it conforms to the CustomStringConvertible
protocolhabitat
, owner
and recordID
of types String
and date
of type Date
Date()
objectdescription
to the class, and return a string value that contains a description, for instance by listing all propertiesAnd here’s how you code that…
First, add the class definition to the file:
class Purchase: CustomStringConvertible
{
}
Then, add the 4 properties to the class:
var habitat:String = ""
var owner:String = ""
var date:Date = Date()
var recordID:String = ""
See how they’re initialized with empty strings? The Date()
object represents the current date and time.
Finally, add the description
property:
var description:String {
return "recordID = \(recordID), habitat = \(habitat), owner = \(owner), date = \(date)"
}
This is a computed property. Instead of containing a fixed value, it executes a small function when accessed, which returns a dynamic value based on the properties of the Purchase
object.
This description
property is called whenever the object is treated as a string, for instance by print()
, so we can effectively use it to print out a description of Purchase
instances.
We’ll use this Purchase
class throughout the app. Let’s continue!
This description
property is described in the CustomStringConvertible
protocol. You don’t have to implement it. If you don’t, an object of type Purchase
will just show up as “Purchase”. When you do implement it, however, the object will be printed out with a nicely formatted string.
2. Create An API Class
Next up is the API
class. It is responsible for interacting directly with the API.
We’re creating a separate class for this purpose, so it doesn’t get all tangled up in the rest of our code. We’re using the Singleton pattern to give access to one shared API instance everywhere in our code.
API.swift
. import UIKit
import Alamofire
This allows us to use code from the Alamofire
library in this file. If you haven’t yet integrated Alamofire in your project, go back to the relevant previous step.
Then, add the class itself:
class API
{
}
That’s pretty basic! Let’s spice it up with that shared instance. Add this to the class body:
static let shared = API()
This is a static constant, also known as a class property. It’s available on the class API
, instead of on an instance of API
. We can use it anywhere in our code like this:
API.shared.someFunction()
Interestingly, we’re referencing an instance of the class API
on the class API
. That’s why it’s often referred to as a shared instance.
Don’t misuse the shared instance or Singleton pattern! It’s easy to create a God class with all sorts of code and give access to it anywhere in your code, but that’s not the purpose of a singleton. You should only use the Singleton pattern for objects that you need only one instance of, that has one single purpose, that contains code that should be accessible anywhere in your app, and only when that code doesn’t belong anywhere else.
3. Write A “getAllRecords()” Function
The blockchain app works like this:
So, we need a way to get all records from the blockchain. The Tierion API has an endpoint for that at https://api.tierion.com/v1/records.
You provide that endpoint with a datastoreId
parameter, and some authentication data, and it returs the last 100 records. It lists the most recent first, and we can use that to find the current owner of a habitat.
But first, some housekeeping!
In the blockchain app we’re going to interact with 3 endpoints of the Tierion API. Every time we do that we’ll need to provide authentication headers.
These two:
X-Username: bob@crypto.com
X-Api-Key: WW91IGhhZCB0byBrbm93IHdoYXQncyBpbiBoZXJlLCBkaWRuJ3QgeW91IT8=
It also makes sense to save the Datastore ID somewhere, and the URL of the API. So, as the perfectly organized developers we are, what do we do? We make a struct
for configuration values, of course!
struct Constants
{
static let url = "https://api.tierion.com/v1"
static let datastoreID = "..."
static let username = "..."
static let key = "..."
}
You can add it above (not inside!) the API
class, or put it in its own Constants.swift
file. Make sure you fill in the correct values:
datastoreID
is the Datastore ID you wrote down earlierusername
is the email address of your Tierion accountkey
is the Tierion API Key you wrote down earlierAnywhere in your code you can use these values like this:
Constants.datastoreID
OK, now we’re finally going to write that getAllRecords()
function. Add it to the API
class, like this:
func getAllRecords()
{
}
Then, add this behemoth of a function call inside the getAllRecords()
function body:
Alamofire.request(Constants.url + "/records", method: .get, parameters: ["datastoreId": Constants.datastoreID], encoding: URLEncoding.default, headers: headers).responseJSON { [unowned self] response in
}
Whoah! Let’s deconstruct that. In short, this function sends a GET request to the Tierion API, with a few parameters.
Here’s what it does:
request(...)
function on the Alamofire
framework. It has 5 parameters:
String
method
is set to .get
, which indicates that this is a HTTP(S) GET requestparameters
is a dictionary with… parameters. The keys and values in this dictionary form the URL query string. They’re like variables you can add to a URL, like https://api.tierion.com/v1/records?datastoreId=1234
encoding
tells Alamofire we want to encode the parameters collection using URL Encoding. Other options include JSON, for instance.headers
contains a dictionary with HTTP request headers. It’s using a variable headers
that we’ll code later.request(...)
function is neatly chained to another function called responseJSON(completionHandler:)
. We provide it with a closure, also called a completion handler. This closure is executed when the HTTP request completes, so when a response has come back from the Tierion API. The closure has one parameter response
of generic type DataResponse<Any>
.completionHandler
closure, because it’s the last argument. This is called a trailing closure. The [unowned self]
is called a capture list, and we’re indicating that this closure shouldn’t strongly retain self
.If your app has become a hot stinking mess of nested callbacks, also known as “callback hell”, then I recommend you check out how Promises work.
You’ll still need to code that headers
variable. You can add it to the API
class like this:
let headers:HTTPHeaders = [
"X-Username": Constants.username,
"X-Api-Key": Constants.key
]
This makes the headers
constant available for any function that’s part of the API
class, so we can reuse those headers in different parts of the class. If you look closely, you’ll see that the headers
constant contains the constants we defined earlier. They’re now used as HTTP request headers.
Pfew! And that’s just the function call – now we’re going to implement that closure…
The above screenshot is of the Mac app Paw. It’s simply the best app to inspect, test and document webservices and REST APIs.
In the above screenshot you can see the JSON response from the /records
API endpoint. This is the data we’re working with:
{
"accountId": 1234,
...
"records": [
{
"id": "2jTcN3Infk6zDTY0YbVweg",
"label": "Copernicus;Arthur Dent",
"timestamp": 1524333917
},
{
"id": "pYiI1hYvNkG2KxwBrSueFA",
"label": "Schiaparelli;Mark Watney",
"timestamp": 1524333144
}
]
}
The JSON has one root element, an object, with an array records
that contains objects that have an id
, label
and timestamp
.
id
is the Record ID of the Tierion API that identifies a blockchain recordlabel
is a textual representation of the object, we’ll use it to save the habitat and owner with a [habitat];[owner]
formattimestamp
is a simple Unix timestamp of the record’s creation date and timeYou can read more about Tierion’s blockchain API in their documentation.
Remember how we’re using the blockchain to match habitats with their owners? When processing the JSON, all we really need to do is decompose the above data format and turn that into an array of Purchase
objects.
Let’s do it step by step. In the next few paragraphs I’ll show you how to code this part of the app. The complete implementation of the getAllRecords()
comes after that.
First, we need a way to save any Purchase
objects we create. So add the following instance property to the API
class:
var purchases = [Purchase]()
Easy, right? You can add it right after let headers ...
and right before the func getAllRecords() ...
. The property is initialized as an empty array of Purchase
objects.
Note: In a properly architected blockchain app you wouldn’t use the API
class to store structured data. Instead, you’d use Realm or Core Data, and save objects in the database. For the purposes of this tutorial however, we’ll use the above purchases
array as our local datastore.
Here’s that HTTP request again. Inside the closure we have access to self
and response
, of type DataResponse<Any>
.
Alamofire.request(...).responseJSON { [unowned self] response in
// Do magic stuff...
}
Then, you need to get to the JSON data in response
, and the record
array in that JSON data. Like this:
if let json = response.result.value as? [String: Any],
let records = json["records"] as? [[String: Any]]
{
}
What happens here?
response.result.value
and cast it from Any?
to the dictionary type [String: Any]
records
data by its key "records"
and cast it from Any
to [[String: Any]]
or “array of dictionaries with string keys and any values”.You also use conditional binding to safely cast. The conditional body won’t execute when the cast fails, because it then returns nil
.
How do you know what the types of the JSON data are?
String
keys.Any
type in Swift"record"
element is an array of dictionaries.[String: Any]
and the type of the records
array is [[String: Any]]
A trivia question for you: how do we know that the type of json["records"]
is Any
before casting it?
Then, the next thing we do is empty the purchases
array, to start with a clean slate.
self.purchases.removeAll()
Why do we write purchases.removeAll()
inside the if let
conditional statement, instead of outside it, or even outside the Alamofire.request(...)
code? That’s because inside the conditional we’re fairly certain that the JSON data has been received in good order. And we don’t run the risk of removing the data we have, only to discover that the new data is incomplete or malformed.
Then, we loop over every item in the records
array. After all, you want to process every record in the JSON! Do you know the type of item
?
for item in records
{
}
Then, for every item we find we want to create a new Purchase
object. Like this:
let purchase = Purchase()
Then, we want to get the Record ID and add it to the purchase
object. Like this:
if let recordID = item["id"] as? String {
purchase.recordID = recordID
}
What kind of magic is that!?
records
item has an "id"
key, so we get that with item["id"]
Any
, but we know it is actually a string, so we cast it to String
and treat it as suchrecordID
constant to the recordID
property on the purchase
objectOK, let’s do it again! But then for the "timestamp"
of item
. Like this:
if let timestamp = item["timestamp"] as? Double {
purchase.date = Date(timeIntervalSince1970: timestamp)
}
This time we’re casting to Double
so we can use that value to initialize a Date
object with the timestamp. As a result, the Swift Date
object now points to the correct date and time.
Then, the label… That’s a bit more complicated! Remember how we’re using the "label"
on an item
as a shorthand to store the habitat name and owner?
Tierion’s API doesn’t return the complete blockchain object for the /records
API endpoint. We need that information, so that’s why we’re kinda misusing the label for another purpose. You’ll see later on in this tutorial how we’re adding the data to that label.
First, let’s get the label
as a String
from the JSON item
:
if let label = item["label"] as? String
{
}
Any label
value follows the [habitat];[owner]
format. Like this:
"Schiaparelli;Mark Watney"
If we want to decompose the habitat
and owner
from that, the only thing we need to do is split the string on the ;
character, and get the first and second elements.
We do that with the split(separator:)
function on String
, and the use the higher-order function map(_:)
to turn that into a String
object. (The split(...)
function returns a value of type Substring
.)
Like this:
let a = label.split(separator: ";").map { String($0) }
The a
array now has this value: ["Schiaparelli", "Mark Watney"]
.
Nice! So the one thing we then do is simply add those values to the purchase
object. Like this:
purchase.habitat = a[0]
purchase.owner = a[1]
Then, the entire block of code now looks like this:
if let label = item["label"] as? String
{
let a = label.split(separator: ";").map { String($0) }
if a.count != 2 {
continue
}
purchase.habitat = a[0]
purchase.owner = a[1]
}
Note: We’re adding a little error correction in there with if a.count != 2
to avoid adding purchases to the array that accidentally don’t follow the [habitat];[owner]
format. For instance, when someone has a name with a semi-colon in it…
Then, the last thing we’re coding in the for-in
loop is this bit:
self.purchases += [purchase]
We’re simply adding the purchase
object, that we’ve just filled with values, to the purchases
property on the API
instance. Ultimately, this will fill up that array with purchases found in the JSON data.
There are two more things left to do:
purchases
First, sorting the purchases
array. Why would we do that? Note that the following is true:
purchases
array contains purchases of habitats, identified by their ownersHow do we know who the current owner is? Think about it. I’ll wait.
…
Got it? It’s the last owner in the purchases
array, if you sort it by most recent purchases first.
And as it turns out, that’s easy to do in Swift:
self.purchases.sort { $0.date > $1.date }
In the above code we’re using the sort(_:)
function. Without going into it too deeply – the sort(_:)
function takes a closure. Within the closure you compare any two values and return whichever is greater.
In the code above we’re sorting the most recent date first, so the most recent purchases end up first in the array.
Finally, we want to notify the table view controller that its data has been updated. We can do so by using Notification Center, a super useful Swift class that you can use to broadcast information between components of your app that aren’t directly related to each other.
Add the following code to the function, outside the for-in
loop:
NotificationCenter.default.post(name: .recordsUpdated, object: nil)
You can also add the following extension to the API.swift
file, or its own file, so we can use that notification name as .recordsUpdated
.
extension Notification.Name
{
static let recordsUpdated = Notification.Name("recordsUpdated")
}
With the post(...)
function you’re posting a notification to the Notification Center. Any observer that’s listening in for that type of notification will get subsequently notified by the Notification Center. It’s ideal for broadcasting one-to-one and one-to-many signals between components of your app.
Ultimately, the getAllRecords()
function now looks like this:
func getAllRecords()
{
Alamofire.request(Constants.url + "/records", method: .get, parameters: ["datastoreId": Constants.datastoreID], encoding: URLEncoding.default, headers: headers).responseJSON { [unowned self] response in
if let json = response.result.value as? [String: Any],
let records = json["records"] as? [[String: Any]]
{
self.purchases.removeAll()
for item in records
{
let purchase = Purchase()
if let label = item["label"] as? String
{
let a = label.split(separator: ";").map { String($0) }
if a.count != 2 {
continue
}
purchase.habitat = a[0]
purchase.owner = a[1]
}
if let recordID = item["id"] as? String {
purchase.recordID = recordID
}
if let timestamp = item["timestamp"] as? Double {
purchase.date = Date(timeIntervalSince1970: timestamp)
}
self.purchases += [purchase]
}
self.purchases.sort { $0.date > $1.date }
NotificationCenter.default.post(name: .recordsUpdated, object: nil)
}
}
}
You’ve just coded all that, but here’s what happens in short, again:
response
parameterrecords
from the JSON data, and iterate over every element in the records
array with for-in
recordID
, label
and timestamp
, and add those to a Purchase
object, which is subsequently added to the purchases
instance propertypurchases
so they’re organized most recent firstThat wasn’t so bad, was it? Let’s continue!
4. Respond To Notifications From “getAllRecords()”
Now that we’ve written the code to get the records from the Tierion API, we can put that code to use.
Here’s what we’ll do:
getAllRecords()
getAllRecords()
All this happens in the table view controller class HabitatTableViewController
, so make sure you open that file in Xcode.
First, write the following line of code in viewDidLoad()
. You can add it right after tableView.register(...
:
NotificationCenter.default.addObserver(self, selector: #selector(onRecordsUpdated(_:)), name: .recordsUpdated, object: nil)
This code registers the current instance of the table view controller as an observer for the .recordsUpdated
notification.
When that notification is posted to the Notification Center, the function onRecordsUpdated(_:)
of the table view controller is called. (We’ll code that function later.)
Note the syntax of addObserver(_:selector:name:object:)
. See how we’re telling Notification Center to call onRecordsUpdated(_:)
on self
when the notification .recordsUpdated
is posted?
Then, write the following line of code below addObserver(...)
:
API.shared.getAllRecords()
This initiates the API request we coded earlier. So first we’re registering the observer, and then we’re calling the function that ultimately posts the notification.
Finally, let’s code that onRecordsUpdated(_:)
function. Here’s what it does:
onRecordsUpdated
and it has one unnamed parameter notification
of type Notification
@objc
attribute, to become available in the Objective-C runtimeIn Swift code, that’s this:
@objc func onRecordsUpdated(_ notification: Notification)
{
tableView.reloadData()
}
Easy! See how that works?
5. Write A “getLastPurchase(of:)” Function To Get The Habitat Owner
Remember this line of code you wrote in the function set(name:owner:price:)
of HabitatCell
?
ownerLabel?.text = owner == nil ? "For sale!" : "Currently owned by: \(owner!)"
When owner
is nil
, the label text is “For sale!”. When it is not nil
, the label text is “Currently owned by: […]”. We’re using that bit of code to show the user who the owner of a habitat is.
Previously, we structured the JSON data from the /records
endpoint to create Purchase
objects. We also figured out that when the purchases
array is sorted most recent first, we can pick off the first item of a filtered array to find the owner of any habitat.
We’re now going to put those pieces together. Are you up for a challenge? Write a function for the API
class that does the following:
getLastPurchase(of habitat:String) -> Purchase?
, so it takes one named parameter habitat
and returns an optional Purchase
objectpurchases
and find the Purchase
object of which the habitat
property is equal to the habitat
function parameternil
if you can’t find an owner for the habitat, otherwise return the Purchase
object that you foundHere’s the one-line function I came up with:
func getLastPurchase(of habitat:String) -> Purchase?
{
return purchases.first { $0.habitat == habitat }
}
See how that works? It’s almost cheating…
We’re using the first(where:)
function on the purchases
array. Every Swift array has this function. What the function does is simple: it returns the first element of the array for which the where
closure returns true.
The $0
shorthand is the first (and only) parameter of the closure, the current element of the array that we’re inspecting. Its type is Purchase
. We’re assessing whether that element’s habitat
property is equal to the habitat
function parameter.
The first(where:)
function will actually iterate over the purchases
array, and call the closure for every array item. Whenever the result of that closure is true
, the iteration stops and that Purchase
object is returned.
The return type of first(where:)
is the same as the type of the array objects, so that’s Purchase
. It’s also an optional, because first(where:)
can potentially return nil
when it can’t find an item that satisfies the predicate. Therefore, the return type of our function is Purchase?
.
And as usual, we’re using trailing closure syntax to make this function super concise!
That closure mechanism is called a predicate, which is like a “rule” that needs to be satisfied, or a condition that needs to be met.
Can we also code the function more explicitly, without using fancy Swift functions? Of course! Check this out:
func getLastPurchase(of habitat:String) -> Purchase?
{
for purchase in purchases
{
if purchase.habitat == habitat {
return purchase
}
}
return nil
}
See how that works?
For every purchase
in purchases
, assess whether the habitat
property is equal to the habitat
function parameter. If it is, return the purchase
object. If the entire array is iterated without finding a match, the function eventually returns nil
.
What’s even more interesting is that both function implementations use the exact same mechanism for finding a first array item that satisfies a given condition.
Note: The getLastPurchase(of:)
function obviously only works when the purchases
array is sorted properly, i.e. most recent first. You’ll often find that these software engineering problems can be solved by structuring, organizing and filtering data. In fact, that’s what a developer’s job is about 99% of the time!
Now that we can figure out the owner of any habitat, let’s put that function getLastPurchase(of:)
to use!
Switch to HabitatTableViewController.swift
and locate the tableView(_:cellForRowAt:)
function. Locate the set(name:owner:price:)
function call in the function body.
The tableView(_:cellForRowAt:)
function provides table view cells to the table view controller. It’s also the function responsible for giving the cell the right data. So that’s the function that wants to know who the owner of a habitat is, so it can tell the table view cell.
How do we want to change this function?
getLastPurchase(of:)
is not nil
, we want to provide the owner’s name to the cellgetLastPurchase(of:)
is nil
, we can’t provide the owner’s name to the cell, so the name stays nil
like beforeCan you code that?
You’ll want to change that cell.set(...)
line to this:
if let purchase = API.shared.getLastPurchase(of: habitat) {
cell.set(name: habitat, owner: purchase.owner, price: price)
} else {
cell.set(name: habitat, owner: nil, price: price)
}
Here’s how that works:
getLastPurchase(of:)
to purchase
if it’s not nil
habitats
by using indexPath.row
? We’re using that habitat
value as the argument for getLastPurchase(of:)
, so that function can return the most recent Purchase
object that has the same habitat namePurchase
object, we provide the purchase.owner
value to cell.set(...)
, so that the owner’s name is shown in the table view cellPurchase
object, i.e. purchase
is nil
, and the else-clause is executed, we just set that owner
parameter to nil
, as we did beforeThe entire tableView(_:cellForRowAt:)
function now looks like this:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "habitatCell", for: indexPath) as! HabitatCell
if let habitat = habitats[indexPath.row][0] as? String,
let price = habitats[indexPath.row][1] as? Int
{
if let purchase = API.shared.getLastPurchase(of: habitat) {
cell.set(name: habitat, owner: purchase.owner, price: price)
} else {
cell.set(name: habitat, owner: nil, price: price)
}
}
return cell
}
Awesome! When you run your app now with Command-R
or the Play button, it should compile without errors.
You won’t see any owned habitats in the app yet, because we haven’t created any blockchain records yet. Let’s change that in the next chapter!
Important: In a production-ready blockchain app you wouldn’t interact with a public API, such as Tierion’s, directly. Even though the API requests are encrypted with SSL/HTTPS, you can easily extract your API keys and secrets from an app binary. When someone has those, they can create “fake” requests to your Tierion datastore, and basically do whatever they want with it. For the sake of education, we’re directly interfacing with the Tierion API in this iOS tutorial. In your own blockchain app, you’ll want to put a webservice between the app and the Tierion API, so you can use OAuth2 authentication, logging, access control, to create a safer and permissioned blockchain app.
Sign up for our iOS development course Zero to App Store and learn how to build professional iOS 12 apps with Swift 4 and Xcode 10.
The most important feature of your blockchain app is missing: purchasing habitats!
So far, we’ve connected to the blockchain to get data about purchases, and show that data in the app, but we haven’t yet added the functionality needed to make habitat purchases.
Let’s change that. Here’s what we’ll do:
createRecord(habitat:owner:)
function to store purchases.createRecordReceived
notifications and respond to thempurchase(habitat:)
function to initiate purchasespurchase(habitat:)
function in tableView(_:didSelectRowAt:)
Sounds good? Let’s continue.
1. Write A “createRecord(habitat:owner:)” Function
When a user of your blockchain app wants to purchase a habitat, what they’re really doing is storing a record in the blockchain. That record is the proof that they purchased the habitat.
So, if we want to enable the user to purchase a habitat, we need a way to store records in the blockchain. And what better place to do so than in our API
class?
Here’s what you’re going to code:
func createRecord(habitat: String, owner: String)
in the API
class.post
request to the /records
endpoint of the Tierion APIdatastoreId
, habitat
, owner
and firstname
(label) parameters, encoded as JSON. Don’t forget to send the headers!.createRecordReceived
to Notification Center, together with the habitat
, owner
and record status
valuesIf you can do that on your own, go for it! If not, bear with me and I’ll walk you through it.
First, make sure you’re editing the API.swift
file. Add the following function to the class:
func createRecord(habitat: String, owner: String)
{
}
Then, let’s set up the parameters of the HTTP request. Like this:
let parameters = [
"datastoreId": Constants.datastoreID,
"habitat": habitat,
"owner": owner,
"firstname": "\(habitat);\(owner)"
]
It’s a simple dictionary that contains values for the Datastore ID, and three fields: habitat
, owner
and firstname
. Note that we’re using the habitat
and owner
function parameters, and the datastoreID
constant.
That firstname
parameter is what ends up in the label
field of the response in getAllRecords()
. Remember how we used split(separator:)
to decompose the label
field?
In the above code we set that semicolon format [habitat];[owner]
with:
...
"firstname": "\(habitat);\(owner)"
...
Unfortunately, the Tierion API doesn’t return all the custom fields of our datastore records for the Get All Records request. Instead, it provides a generic “label” for every record. We can change the value of that “label” by (mis)using the firstname
field of the Create Record request.
Next, let’s set up the HTTP POST request for the Create Record endpoint. Like this:
Alamofire.request(Constants.url + "/records", method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: headers).responseJSON { response in
}
Just as before, we’re using Alamofire to set up the request, and attach a response handler with responseJSON(...)
with a closure.
The request(...)
function takes the request URL, the request method .post
, that parameters
array, a encoding setting for JSON, and the headers
property. Within the closure we have access to the parameter response
that contains the request response data.
Then, inside the closure, we are going to decompose the response data. Like this:
if let json = response.result.value as? [String: Any],
let status = json["status"] as? String
{
}
This is similar to what we did in the previous request handler. The response JSON data is of type [String: Any]
, and we’re trying to get to the json["status"]
value, and cast it to String
.
Here, this is what the JSON data looks like:
See how status
is a top-level element of the JSON response? And we’re providing those parameters
in the request body, too.
The status, by the way, has 3 possible values:
queued
, this means that the record is queued to be added to the blockchainunpublished
, this means that the record has been processed, but hasn’t been published to the blockchain yetcomplete
, this means that the record has been added to the blockchain, and has been confirmed by the blockchain mechanismThe difference between unpublished and published is exactly what the Proof of Work, that we talked about earlier, is about!
OK, back to the code. We now want to broadcast a notification with the relevant data of this purchase, including the blockchain record’s status. Here’s how:
let userInfo = [
"status": status,
"habitat": habitat,
"owner": owner
]
NotificationCenter.default.post(name: .createRecordReceived, object: nil, userInfo: userInfo)
You also want to add the following Notification.Name
to the extension you created earlier, so you can use .createRecordReceived
:
static let createRecordReceived = Notification.Name("createRecordReceived")
The entire createRecord(habitat:owner:)
function now looks like this:
func createRecord(habitat: String, owner: String)
{
let parameters = [
"datastoreId": Constants.datastoreID,
"habitat": habitat,
"owner": owner,
"firstname": "\(habitat);\(owner)"
]
Alamofire.request(Constants.url + "/records", method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: headers).responseJSON { response in
if let json = response.result.value as? [String: Any],
let status = json["status"] as? String
{
let userInfo = [
"status": status,
"habitat": habitat,
"owner": owner
]
NotificationCenter.default.post(name: .createRecordReceived, object: nil, userInfo: userInfo)
}
}
}
2. Observe “createRecordReceived” Notifications And Respond
Whenever a purchase is initiated, and the request successfully completes, that .createRecordReceived
notification is posted to the Notification Center.
If we observe those notifications, we can respond to them, for instance by confirming the purchase and displaying the blockchain record status.
Here’s roughly how:
.createRecordReceived
notificationonCreateRecordReceived(_:)
functionLet’s code that!
First, open the HabitatTableViewController
class and locate the viewDidLoad()
function. Add this line of code:
NotificationCenter.default.addObserver(self, selector: #selector(onCreateRecordReceived(_:)), name: .createRecordReceived, object: nil)
See how we’re adding an observer for .createRecordReceived
? When that notification is broadcasted, the function onCreateRecordReceived(_:)
on self
is called. And self
is the current instance of the table view controller, of course.
Next up, add this function to the class:
@objc func onCreateRecordReceived(_ notification: Notification)
{
}
Pretty self-explanatory, right? OK, then add this line of code to the function:
API.shared.getAllRecords()
That’s important! Whenever a purchase is made, we want to update the table view with new habitat-owner data. It makes no sense to just reload the table view – no, we need to get the data from the Tierion API.
Next up, we’re decomposing the userInfo
property of the notification
parameter. Like this:
if let status = notification.userInfo?["status"] as? String,
let habitat = notification.userInfo?["habitat"] as? String,
let owner = notification.userInfo?["owner"] as? String
{
}
Remember when we created that userInfo
dictionary in createRecord(...)
, to send together with the .createRecordReceived
notification? This is that exact same data.
In short, we’re using conditional binding to get the values from userInfo
, while using conditional casting at the same time. When all three casts succeed, the conditional executes, and we can use the values from status
, habitat
and owner
.
Don’t forget that when you’re casting the data doesn’t change, only the way you treat that data – with a different type. Casts fail when a value can’t be treated like the type you’re trying to cast it as. Don’t worry, casting used to confuse the beeswax out of me until I made the analogy with my uncle dressing up as Santa Claus…
Are we there yet? No! Let’s construct that alert dialog and show it to the user. Like this:
let message = "status = \(status), habitat = \(habitat), owner = \(owner)"
let alert = UIAlertController(title: "Purchase created in blockchain!", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
self.present(alert, animated: true)
That’s simple code. It creates a message that includes the purchase data, and then shows that to the user by using an alert controller.
3. Write A “purchase(habitat:)” Function To Initiate Purchases
If you’re getting the feeling that we are building this blockchain app backwards, then you’d be right. You first write a function, and only then you can use it!
Even though our blockchain app is primarly used to purchase habitats, we’ve written a whole lot of code to support the purchasing of habitats.
In fact, we’ve just written the code that responds to the purchase of habitats. It’s only just now that we’re ready to actually make those purchases.
OK, let’s do it.
First, the code that makes the purchase. Add the following function to the HabitatTableViewController
class:
func purchase(habitat: String)
{
}
This function purchase(habitat:)
has one parameter of type String
, the name of the habitat.
Then, you’re going to create that alert dialog. We’re using the dialog to ask for the name of the new habitat owner.
Creating and displaying an alert dialog has three steps:
UIAlertController
objectFirst, let’s create the UIAlertController
object. Like this:
let alert = UIAlertController(title: "What is the name of the new owner of \(habitat)?", message: nil, preferredStyle: .alert)
Then, we’re adding a simple “Cancel” button to it. It doesn’t do anything, except dismiss the alert dialog. Like this:
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
Then, add a text field to the alert controller. Like this:
alert.addTextField(configurationHandler: { textField in
textField.placeholder = "Input your name here..."
})
Text fields are added to a simple array that’s part of the alert controller, and you can use the closure, as seen above, to configure the text field.
Then, let’s add the “Purchase” button to the alert dialog. Like this:
alert.addAction(UIAlertAction(title: "Purchase", style: .default, handler: { action in
if let owner = alert.textFields?.first?.text {
API.shared.createRecord(habitat: habitat, owner: owner)
}
}))
Easy, right? When the button is tapped, the closure is executed. In the closure we’re getting the text from the text field by using the alert.textFields
array.
We then use that owner
string to call createRecord(habitat:owner:)
on the API. This effectively creates a new record in the Tierion blockchain!
And don’t forget to present the alert dialog:
self.present(alert, animated: true)
The entire purchase(habitat:)
function now looks like this:
func purchase(habitat: String)
{
let alert = UIAlertController(title: "What is the name of the new owner of \(habitat)?", message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
alert.addTextField(configurationHandler: { textField in
textField.placeholder = "Input your name here..."
})
alert.addAction(UIAlertAction(title: "Purchase", style: .default, handler: { action in
if let owner = alert.textFields?.first?.text {
API.shared.createRecord(habitat: habitat, owner: owner)
}
}))
self.present(alert, animated: true)
}
Okay, NEXT!
4. Use The “purchase(habitat:)” Function In “tableView(_:didSelectRowAt:)”
Now that we’ve coded that purchase(habitat:)
function, let’s put it to use. We’ll do so by calling it when the user taps a table view cell.
First, add the following function to the HabitatTableViewController
class:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
}
This is another overriden delegate function that’s part of the table view controller mechanism. The function is executed whenever the user taps a cell.
We’ll use it to give the user the option to purchase a habitat. Later on we’ll also hook into this function to let users verify habitat owners.
Next up, we need the name of the habitat that has been tapped. Like this:
if let habitat = habitats[indexPath.row][0] as? String
{
}
This code is similar to what you’ve used in tableView(_:cellForRowAt:)
. You’re using the indexPath
to get the right habitat name from habitats
.
Then, you’re going to create that alert dialog. Create the UIAlertController
object like this:
let alert = UIAlertController(title: "What do you want to do with \(habitat)?", message: nil, preferredStyle: .alert)
Then, add one “Purchase” and one “Cancel” button, and actions that are invoked when the alert button is pressed. Like this:
alert.addAction(UIAlertAction(title: "Purchase", style: .default, handler: { [weak self] action in
self?.purchase(habitat: habitat)
}))
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
The closure is executed when the button is tapped. See how we’re using that purchase(habitat:)
function we wrote earlier? This is where we finally get to use it!
Kind of an anti-climax, isn’t it? You’ll often find that you code abstractions in your app, layer by layer, until the only thing you need to do is call a simple function. It’s magical!
Finally, let’s present that alert dialog:
self.present(alert, animated: true)
The entire function now looks like this:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
if let habitat = habitats[indexPath.row][0] as? String
{
let alert = UIAlertController(title: "What do you want to do with \(habitat)?", message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Purchase", style: .default, handler: { [weak self] action in
self?.purchase(habitat: habitat)
}))
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
self.present(alert, animated: true)
}
}
Awesome! But… does it work? At this point you can run the app with Command-R
or the Play button, and successfully purchase habitats.
You can now purchase a habitat by tapping the cell in the table view, and entering your name in the text field. The table view updates, and the record is queued to get stored in the blockchain.
When you go to your Tierion Dashboard, you can also see records that have been created in the blockchain, and see their attributes. You should be able to check that a blockchain record hasn’t been confirmed yet, too.
It can sometimes happen that purchases don’t immediately show up, even though you’ve just made them. That’s most likely because the responses from the Tierion API are cached for a short while, or simply not committed yet when you refresh your data.
One way to solve that is by implementing a simple pull-to-refresh interaction. Here’s how.
First, add this code to viewDidLoad()
of the HabitatTableViewController
class:
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(onRefreshControlChanged(_:)), for: .valueChanged)
tableView.refreshControl = refreshControl
Then, add that onRefreshControlChanged(_:)
function to the class, too. Like this:
@objc func onRefreshControlChanged(_ refreshControl:RefreshControl)
{
API.shared.getAllRecords()
}
Finally, make sure to add the following line of code to onRecordsUpdated(_:)
. Otherwise the refresh control keeps spinning, even though the data has been successfully refreshed!
tableView.refreshControl?.endRefreshing()
In short, the function getAllRecords()
on the API is called whenever the user pulls on the refresh control, effectively refreshing the table view data. The refresh control resets when the data has been received.
Let’s continue with the next and last step of building the blockchain app.
Now that we’ve completed the biggest aspect of your blockchain app – storing records in the blockchain – we are going to build the feature that makes the blockchain unique: verifying data.
Earlier we discussed proof of work, and how blockchain miners get rewarded for constructing blocks of pending “facts”.
The Tieron blockchain platform takes care of all this for us. When you add a new record to your datastore, it automatically gets queued for storing in the blockchain.
Subsequently, miners pick up the work and incorporate your data in the blockchain. The chain of blocks, and its confirmations, guarantee that your data is irrevocably stored in the blockchain.
Blockchain-as-a-Service platforms don’t store the exact data in the blockchain, but instead store a cryptographic hash value of that data. This has the added benefit of privacy, because a blockchain is public. And because hashes transform data of arbitrary size to fixed values, they’re also smaller than the orginal data.
It can take a few minutes, up to a few hours, to confirm new data that has been added to the blockchain. Because of the way blockchain mining works, it’s uncertain when a new block is mined, and therefore it’s uncertain when a pending fact is successfully added to a newly mined block.
That’s where the status
property of a datastore record, as stored in the Tierion blockchain, comes in. As discussed, it has three options: queued
, unpublished
and complete
. A record has successfully been added to the blockchain, and has been confirmed by it, when the status is complete
.
Said differently, the purchase of a habitat isn’t confirmed until it’s status is complete. Naturally, we want to code something that allows the users of your blockchain app to verify purchases!
Here’s what we’ll do:
getRecord(forID:)
function to get the specifics of a record.getRecordReceived
notifications tableView(_:didSelectRowAt:)
to verify habitat purchasesLet’s get started!
1. Write A “getRecord(forID:)” Function
The getRecord(forID:)
function is very similar to the getAllRecords()
function, except that it gets one specific record instead of all records.
Just as before, we’ll prepare an HTTP GET request with Alamofire, process the data once it comes in, and send a notification to the Notification Center.
First, make sure you’re editing the API
class. Add the following function:
func getRecord(forID recordID:String)
{
}
Easy-peasy! The function getRecord(...)
only has one parameter recordID
, with an argument label forID
. That’s convenient, because now we can use it as: getRecord(forID: "abcd")
.
Next, let’s set up that HTTP GET request. Like this:
Alamofire.request(Constants.url + "/records/" + recordID, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers).responseJSON { response in
}
Again, there’s a couple of parameters:
https://api.tierion.com/v1/records/[recordID]
.get
method to indicate that this is an HTTP GET requestnil
for parameters
encoding
headers
property, as usual, for authenticationresponseJSON(...)
and provide it a completion handler, that has a response
parameterTake a look at the response data for this request:
See how there’s one root object, with values for status
, data
and timestamp
, and how that data
object has values for habitat
and owner
?
Let’s write some code to get that data out. This code, to be exact:
if let json = response.result.value as? [String: Any],
let status = json["status"] as? String,
let timestamp = json["timestamp"] as? Double,
let data = json["data"] as? [String: String],
let habitat = data["habitat"],
let owner = data["owner"]
{
}
The above conditional binding uses the JSON dictionaries, and optional casting, to get to the right data.
If you look closely, you’ll see that it first gets the root-level elements status
, timestamp
and data
. Then, it goes deeper into data
to get habitat
and owner
.
We’re using typical types like String
, Any
and Double
to cast the values to the correct types.
Then, inside the conditional we only have to format the data and post it as a userInfo
dictionary alongside a notification.
Here’s how. First, we’re turning that timestamp
into an ordinary date string. Like this:
let formatter = DateFormatter()
formatter.dateStyle = .short
let date = formatter.string(from: Date(timeIntervalSince1970: timestamp))
Here’s what happens:
DateFormatter
object with a “short” date style, which is usually dd-mm-YY
depending on your locale. Date
object is constructed with the Unix timestamp we got from the JSON response data.Date
object to get a date string.Then, we simply put the data we decomposed into a userInfo
dictionary. Like this:
let userInfo = [
"status": status,
"date": date,
"habitat": habitat,
"owner": owner
]
And then we post the notification, like this:
NotificationCenter.default.post(name: .getRecordReceived, object: nil, userInfo: userInfo)
The entire function now looks like this:
func getRecord(forID recordID:String)
{
Alamofire.request(Constants.url + "/records/" + recordID, method: .get, parameters: nil, encoding: URLEncoding.default, headers: headers).responseJSON { response in
if let json = response.result.value as? [String: Any],
let status = json["status"] as? String,
let timestamp = json["timestamp"] as? Double,
let data = json["data"] as? [String: String],
let habitat = data["habitat"],
let owner = data["owner"]
{
let formatter = DateFormatter()
formatter.dateStyle = .short
let date = formatter.string(from: Date(timeIntervalSince1970: timestamp))
let userInfo = [
"status": status,
"date": date,
"habitat": habitat,
"owner": owner
]
NotificationCenter.default.post(name: .getRecordReceived, object: nil, userInfo: userInfo)
}
}
}
Don’t forget to add that notification name to the Notification.Name
extension! Here it is:
static let getRecordReceived = Notification.Name("getRecordReceived")
Awesome!
2. Observe And Respond To “.getRecordReceived” Notifications
Are we going to listen for those notification broadcasts? Of course we are! Just as before, we’re going to add an observer for .getRecordReceived
and write the function that fires when the notification comes in.
First, make sure you’re editing the HabitatTableViewController
class and locate the viewDidLoad()
function. Add this line of code:
NotificationCenter.default.addObserver(self, selector: #selector(onGetRecordReceived(_:)), name: .getRecordReceived, object: nil)
See how the function onGetRecordReceived(_:)
is called on self
when the .getRecordReceived
notification is observed?
Then, the function itself. Add it to the class:
@objc func onGetRecordReceived(_ notification: Notification)
{
}
In this function, you’re going to decompose the userInfo
collection you constructed earlier, and show its values in a simple alert dialog.
First, let’s get to the right data. Like this:
if let status = notification.userInfo?["status"] as? String,
let date = notification.userInfo?["date"] as? String,
let habitat = notification.userInfo?["habitat"] as? String,
let owner = notification.userInfo?["owner"] as? String
{
}
Again, we’re doing our little conditional binding and optional casting dance to get to the correct values.
Don’t let it fool you, though! Remember that the type of userInfo
is [AnyHashable: Any]?
in this function, so you really need to cast the data to be able to treat it as strings.
Then, inside the conditional, we’re setting up the alert message. Like this:
let title = status == "complete" ? "Ownership verified!" : "Ownership not verified"
let message = "habitat = \(habitat), owner = \(owner), date = \(date), status = \(status)"
The title
shows whether the ownership of this habitat is verified, or that we’re still waiting for confirmation. And the message
shows a simple list of values.
Finally, we’re showing the data to the user with an alert dialog. Like this:
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
self.present(alert, animated: true)
The UIAlertController
has no actions, except for a Cancel button, so it’s just really “FYI”.
3. Adjust “tableView(_:didSelectRowAt:)” To Verify Habitat Purchases
Can we now finally verify if Bob is the true owner of Bob’s Red Fantasy Land in the Schiaparelli crater on Mars? YES!
First, locate the tableView(_:didSelectRowAt:)
function in the HabitatTableViewController
class.
Inside it, you’ve coded an instance of UIAlertViewController
that has two actions: Purchase and Cancel. You’re going to add a third action, Verify.
What it should do, is this:
Purchase
object for this habitatrecordID
of that Purchase
object to get the associated recordYes, it’s that simple. Here’s the code:
if let purchase = API.shared.getLastPurchase(of: habitat)
{
alert.addAction(UIAlertAction(title: "Verify", style: .default, handler: { action in
API.shared.getRecord(forID: purchase.recordID)
}))
}
You can add it between the let alert = ...
line, and the alert.addAction(...
for Purchase.
How does the code work?
Purchase
object from getLastPurchase(of:)
, if there is anyrecordID
property of that Purchase
object to call the getRecord(forID:)
function in the APIAnd then this happens:
BOOM!
Why don’t you try out your app to see if it works?
And that, ladies and gentlemen, is how you code a blockchain app with Swift! Thank you for making it this far, and if you have questions, I’ll see you in the comments below.
If you ask questions that you could have Googled faster than you could write them in the comment box, you owe me an apple pie!
Want to learn more? Check out these external resources:
Want to know more about iOS development? Check out these articles and tutorials:
Enjoyed this article? Please share it!
Hi, I'm Reinder.
I help developers play with code.