How to build a native iOS dApp that uses the Flow blockchain as the backend
Tags: ios, web3, blockchain, tutorial
Published: 2022-07-07 22:59:11

## Introduction 

This is the second and final part of a web3 tutorial about the [Flow blockchain](https://www.onflow.org/). **Understanding the first part is essential and a pre-requisite** because I explain there how to create and test Smart Contracts, the one we developed to create notes, its API, and how to write transactions and scripts to mutate and read data in the blockchain. Please take your time and make sure you understand it before continuing :) 

[First part: A notepad in the Flow blockchain](https://dev.to/theagilemonkeys/web3-tutorial-a-notepad-in-the-flow-blockchain-5458)

So, in this final part, you will learn:
1. **How to deploy smart contracts** in a Flow blockchain network: `testnet`.
2. **How to build an iOS dApp** that communicates with that contract in Flow's `testnet`. For user authentication, the app will use the [Blocto Wallet](https://portto.com), a cross-chain crypto wallet compatible with Flow. I assume you have a basic understanding of iOS programming. We will be using _SwiftUI_ + _Combine_.


## Deploying the Smart Contract

Note that the `NotepadManager` contract we will be using in the iOS dApp is already deployed in a flow account of my own. Remember that you can use any public smart contract in the blockchain, so you don't need to deploy the contract again in every account that wants to use it, as we learned in the previous article. This step is only for learning purposes in case you are interested in how to deploy contracts. **You can skip this section** and go directly to the dApp implementation part.

We are going to deploy the `NotepadManager` contract to the `testnet`, which is nearly identical to the `mainnet`, but we can operate without paying real gas. This is ideal for learning and testing our projects before deploying them to the production network (`mainnet`).

1. [Install the Flow CLI](https://docs.onflow.org/flow-cli/install/)
2. [Generate the account keys](https://docs.onflow.org/flow-cli/generate-keys/). **Remember to safely store the private key and don't share it with anyone.**
3. Create a Testnet flow account [here](https://testnet-faucet.onflow.org) with your public key.
4. Create a folder for your contracts project, open it in the terminal, and do `flow init`. This will create a `flow.json` file with a default configuration.
5. Open your project folder in an editor. I'm using Visual Studio Code, which has an excellent Cadence plugin you can install from the Extensions Marketplace. 
6. Add a new file to your project called `NotepadManager.cdc`. Paste the contract code we developed in the [previous article](https://dev.to/theagilemonkeys/web3-tutorial-a-notepad-in-the-flow-blockchain-5458). You can find it in the [Playground](https://play.onflow.org/ee8594f9-e491-45cb-a9d6-d6ef759dda93?type=account&id=5722cef9-e962-4458-9123-2b7bbb5f2434&storage=0x02), too.
6. Edit the flow.json file so it looks like this: 
```json
{
	"emulators": {},
	"contracts": {
		"NotepadManagerV1": "NotepadManagerV1.cdc"
	},
	"networks": {
		"emulator": "127.0.0.1:3569",
		"mainnet": "access.mainnet.nodes.onflow.org:9000",
		"testnet": "access.devnet.nodes.onflow.org:9000"
	},
	"accounts": {
		"my-testnet-account": {
			"address": "YOUR ACCOUNT ADDRESS (without the '0x' prefix)",
			"key": "YOUR PRIVATE KEY"
		}
	},
	"deployments": {
		"testnet": {
			"my-testnet-account": ["NotepadManagerV1"]
		}
	}
}
```

Note that we emptied the _"emulators"_ field because we won't be using the local emulator but a real network. We also added the _"contracts"_ and _"deployments"_ fields to point to our contract and account. 

Remember to fill the _"address"_ and _"key"_ fields, as specified. You obtained your account address in step #3 and the private key in step #2. Be careful with that key, and don't commit it to a public repository. 

Now that everything is set up, it's time to deploy the contract! In the terminal, in your project folder, do: 
```bash
flow project deploy
```

You'll see something like this: 
```
  Deploying 1 contracts for accounts: my-testnet-account
  NotepadManagerV1 -> 0x9bde7238c9c39e97 (09d8818a9d41ae2fc92f9dd6d7355772e7d97e28fb9e20b2f66f473420a16b31) 
``` 

Which can be read as: 

```
NotepadManagerV1 -> the address containing it (the transaction ID)
```

You can see the transaction status or the account details through blockchain explorers like [Tesnet Flowscan](https://testnet.flowscan.org) or [Flow View Source](https://flow-view-source.com). For example, if I introduce my account address `0x9bde7238c9c39e97` in Flowscan:

![Flowscan](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kibuozxuwiumsvqtfu8v.png) 

You can see the account balance, transaction history, and contracts deployed to the account. If you click the `NotepadManagerV1` link, you can see the contract's code! This is powerful for public audits of the contracts in the blockchain.


## Implementation

Our iOS dApp will have a pretty straightforward architecture:
![dApp Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vxf1us7da2ebrbdkjs3l.png)

All the elements in that picture are `.swift` files. You can start by creating an Xcode project (iOS 15.5 for the time being) and all those files. Files with the `...View` suffix are SwiftUI views.

For communicating with the Flow blockchain, we will be using the [iOS SDK](https://github.com/Outblock/fcl-swift) that [Outblock](https://outblock.io) is building. A big update containing breaking changes is planned to be released soon, so we are going to lock now the SDK version to a particular commit (latest as of today) to be sure our code keeps working overtime. To do that, just add the dependency of the Swift Package Manager tied to the `b4a9cfaaa0003f3f250490e6ccdf8b75065199a3` commit:
![Package Dependency](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xmi42stvghmuwtplgulr.png)

Now we can start adding code to our Swift files! 

### `FlowNotesApp.swift`

This is the entry point of our app. It's in charge of initializing the fcl-swift framework with the needed endpoints to communicate with the Flow blockchain and Blocto Wallet and showing the `LoginView` or the `MyNotesView` depending on the user's authorization state. 

```swift 
import Combine
import FCL
import SwiftUI

enum UI {
    static let flowGreen = Color(red: 0, green: 1, blue: 130/255)
}

@main
struct FlowNotesApp: App {
    @State private var currentUser: User?
    
    init() {
        // FCL configuration with the Tesnet Nodes URLs needed 
        // to stablish the connection with the blockchain and 
        // the Blocto Wallet:
        fcl.config(appName: "FlowNotes",
                   appIcon: "https://placekitten.com/g/200/200",
                   location: "https://foo.com",
                   walletNode: "https://fcl-discovery.onflow.org/testnet/authn",
                   accessNode: "https://access-testnet.onflow.org",
                   env: "testnet",
                   scope: "email",
                   authn: "https://flow-wallet-testnet.blocto.app/api/flow/authn")
        
        let flowGreenColor = UIColor(cgColor: UI.flowGreen.cgColor ?? UIColor.green.cgColor) // Color to UIColor conversion
        UINavigationBar.appearance().largeTitleTextAttributes = [.foregroundColor: flowGreenColor]
        UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: flowGreenColor]
    }
    
    var body: some Scene {
        WindowGroup {
            VStack {
                if currentUser != nil {
                    MyNotesView(vm: MyNotesVM())
                } else {
                    LoginView()
                }
            }
            .preferredColorScheme(.dark)
            .onReceive(fcl.$currentUser) { user in
                currentUser = user
            }
        }
    }
}
```

### `LoginView.swift`
![LoginView](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kx6b8eq69avpf1yp2ea7.png)

Simple screen with a big login button that invokes the `authenticate()` method on the `LoginVM` _ViewModel_. 

### `LoginVM.swift`

![Blocto Wallet authentication](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ku3xayaa8yxas713053w.png)

The `authenticate()` method will invoke the FCL's authentication method, which will present us with the Blocto Wallet webView for the user to log in. After successful authentication, the FCL's `currentUser` property will be populated, triggering an update in our _FlowNotesApp_ class, which is listening to that property. The `MyNotesView` will be shown then.

```swift
import Combine
import FCL
import Foundation

final class LoginVM: ObservableObject {
    
    private var cancellables = Set<AnyCancellable>()
    
    func authenticate() {
        fcl
            .authenticate()
            .receive(on: DispatchQueue.main)
            .sink { result in
                print(result)
            } receiveValue: { response in
                print(response)
            }
            .store(in: &cancellables)
    }
}
```

### `MyNotesView.swift`

![MyNotesView](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bdpj1uhz5t8t44oetscs.png)

This view is in charge of showing the list of notes retrieved by `MyNotesVM`. The users can also: 
- Delete notes with the swipe-to-left gesture.
- Present the `NoteEditorView` from the "+" button to create new notes. 
- Show the action menu from the "..." button. Here, users can manually refresh the list, delete the entire Notepad or sign out. 

You will note that the transactions are pretty slow compared to a regular backend. This is because mutations of the blockchain need to be accepted and mined, which could take several seconds to happen. For simplicity, we present a full-screen loading screen while the VM's state is `loading`, until the transaction is sealed and a script can safely retrieve the final result. Read-only operations (scripts) like the notes fetch are blazing fast. If the transaction fails with an error, we present it to the user with an `AlertView`.

As you see, the transactions and script code used here are nearly identical to the ones in the first article (I omitted the comments in these to avoid repetition):

```swift
import SwiftUI

struct MyNotesView: View {
    
    @StateObject private var vm: MyNotesVM
    
    @State private var isLoading = false
    @State private var isShowingNoteEditor = false
    @State private var isShowingErrorAlert = false
    
    init(vm: MyNotesVM) {
        _vm = .init(wrappedValue: vm)
    }
    
    private var signOutButton: some View {
        Button {
            vm.signOut()
        } label: {
            Label("Sign Out", systemImage: "person.crop.circle.badge.xmark")
        }
    }
    
    private var refreshNotepadButton: some View {
        Button {
            vm.queryNotes()
        } label: {
            Label("Refresh notepad", systemImage: "arrow.clockwise")
        }
    }
    
    private var deleteNotepadButton: some View {
        Button {
            vm.deleteNotepad()
        } label: {
            Label("Delete notepad", systemImage: "trash")
        }
        .opacity(vm.notes != nil ? 1 : 0)
        .disabled(vm.notes == nil)
    }
    
    private var moreOptionsButton: some View {
        Menu {
            refreshNotepadButton
            Divider()
            deleteNotepadButton
            Divider()
            signOutButton
        } label: {
            Image(systemName: "ellipsis")
        }
    }
    
    private var addNoteButton: some View {
        Button {
            isShowingNoteEditor = true
        } label: {
            Image(systemName: "plus")
        }
    }
    
    var body: some View {
        ZStack {
            NavigationView {
                Group {
                    if let notes = vm.notes {
                        List {
                            ForEach(notes, id: \.id) { note in
                                NoteRow(note: note)
                                    .listRowBackground(Color.black)
                            }
                            .onDelete { index in
                                vm.deleteNote(atIndex: index.first)
                            }
                        }
                    
                    } else if vm.state == .loading {
                        EmptyView()
                    } else {
                        Text("Your notepad is empty 🤓\nAdd a new note from\nthe'＋'button")
                            .multilineTextAlignment(.center)
                            .font(.headline)
                            .foregroundColor(.gray)
                    }
                }
                .navigationBarItems(leading: moreOptionsButton, trailing: addNoteButton)
                .navigationTitle("Flow Notes")
            }
            .tint(UI.flowGreen)
            
            ZStack {
                Color.black
                    .edgesIgnoringSafeArea(.all)
                    .opacity(isLoading ? 0.7 : 0)
                
                ProgressView {
                    Text("On it! 🚀 \nPlease wait...")
                        .multilineTextAlignment(.center)
                        .foregroundColor(.white)
                }
                .progressViewStyle(CircularProgressViewStyle(tint: .white))
                .opacity(isLoading ? 1 : 0)
            }
        }
        .onChange(of: vm.state) { newValue in
            isLoading = newValue == .loading
            isShowingErrorAlert = newValue.isFailed
        }
        .alert(isPresented: $isShowingErrorAlert, content: {
            Alert(title: Text("Error!"),
                  message: Text(vm.state.errorMessage ?? "Unknown"),
                  dismissButton: .default(Text("OK"), action: {
                vm.currentErrorDidDismiss()
            }))
        })
        .sheet(isPresented: $isShowingNoteEditor) {
            isShowingNoteEditor = false
        } content: {
            NoteEditorView().environmentObject(vm)
        }
    }
}

struct NoteRow: View {
    let note: Note
    
    var body: some View {
        HStack {
            VStack(alignment: .leading, spacing: 5) {
                Text(note.title)
                    .foregroundColor(.black)
                    .font(.body.weight(.bold))
                
                Text(note.body)
                    .foregroundColor(.black)
                    .font(.caption)
            }
            
            Spacer()
        }
        .padding()
        .background(UI.flowGreen)
        .cornerRadius(10)
    }
}

private struct PreviewWrapper: View {
    
    var vm = MyNotesVM()
    
    init() {
        vm.notes = [Note(id: 0, title: "This is a great note", body: "This is the body of a great note."),
                    Note(id: 1, title: "Another note", body: "The best body that was ever written.")]
    }
    
    var body: some View {
        MyNotesView(vm: vm).preferredColorScheme(.dark)
    }
}

struct MyNotesView_Previews: PreviewProvider {
    static var previews: some View {
        PreviewWrapper()
    }
}
```

### `MyNotesVM`

This is the VM that defines the set of operations available to mutate the notepad through transactions or fetch the notes through a script. 

Every time you send a transaction, the SDK will present a transaction confirmation screen (webView) from the Blocto Wallet, so you can review the transaction's code and approve it:

![Transaction Confirmation Screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/29xzckyg5t3xaexa7r4g.png)

```swift 

import Combine
import FCL
import Flow
import Foundation

enum LoadingState: Equatable {
    case idle
    case loading
    case didFail(error: String)
    
    var isFailed: Bool {
        guard case .didFail = self else { return false }
        return true
    }
    
    var errorMessage: String? {
        guard case let .didFail(error) = self else { return nil }
        return error
    }
}

final class MyNotesVM: ObservableObject {
    
    @Published private(set) var state = LoadingState.idle
    @Published var notes: [Note]?
    
    private let notepadManagerAddress = "0x9bde7238c9c39e97"
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        defer {
            queryNotes()
        }
    }
    
    // MARK: - Public API
    
    func createNote(title: String, body: String) {
        guard fcl.currentUser?.addr != nil else { return }
        
        state = .loading
        
        fcl.mutate {
            cadence {
                // Transaction that checks if the Notepad 
                // exists (and creates it if needed) before 
                // creating and adding the note to it:
                 """
                 import NotepadManagerV1 from \(notepadManagerAddress)
                 
                 transaction {
                     prepare(acct: AuthAccount) {
                         var notepad = acct.borrow<&NotepadManagerV1.Notepad>(from: /storage/NotepadV1)

                         if notepad == nil { 
                             // Create it and make it public
                             acct.save(<- NotepadManagerV1.createNotepad(), to: /storage/NotepadV1)
                             acct.link<&NotepadManagerV1.Notepad>(/public/PublicNotepadV1, target: /storage/NotepadV1)
                         }
                 
                         var theNotepad = acct.borrow<&NotepadManagerV1.Notepad>(from: /storage/NotepadV1)
                         theNotepad?.addNote(title: "\(title)", body: "\(body)")
                     }
                 }
                 """
            }
            
            gasLimit {
                1000
            }
        }
        .receive(on: DispatchQueue.main)
        .sink { completion in
            if case let .failure(error) = completion {
                self.state = .didFail(error: error.localizedDescription)
            }
        } receiveValue: { [weak self] transactionId in
            guard let self = self else { return }
            
            self.waitForSealedTransaction(transactionId) {
                self.queryNotes()
            }
        }
        .store(in: &cancellables)
    }
    
    func deleteNote(atIndex index: Int?) {
        guard fcl.currentUser?.addr != nil, let index = index, let idToDelete = notes?[index].id else { return }
                
        state = .loading
        
        fcl.mutate {
            cadence {
                // Transaction that tries to delete the note 
                // if the Notepad exists:
                 """
                 import NotepadManagerV1 from \(notepadManagerAddress)
                 
                 transaction {
                     prepare(acct: AuthAccount) {
                            let notepad = acct.borrow<&NotepadManagerV1.Notepad>(from: /storage/NotepadV1)
                            notepad?.deleteNote(noteID: \(idToDelete))
                     }
                 }
                 """
            }
            
            gasLimit {
                1000
            }
        }
        .receive(on: DispatchQueue.main)
        .sink { completion in
            if case let .failure(error) = completion {
                self.state = .didFail(error: error.localizedDescription)
            }
        } receiveValue: { [weak self] transactionId in
            guard let self = self else { return }
            
            self.waitForSealedTransaction(transactionId) {
                self.queryNotes()
            }
        }
        .store(in: &cancellables)
    }
        
    func deleteNotepad() {
        guard fcl.currentUser?.addr != nil else { return }
        
        state = .loading
        
        fcl.mutate {
            cadence {
                // Transaction that deletes the Notepad:
                 """
                 import NotepadManagerV1 from \(notepadManagerAddress)
                 
                 transaction {
                     prepare(acct: AuthAccount) {
                         var notepad <- acct.load<@NotepadManagerV1.Notepad>(from: /storage/NotepadV1)!
                         NotepadManagerV1.deleteNotepad(notepad: <- notepad)
                     }
                 }
                 """
            }
            
            gasLimit {
                1000
            }
        }
        .receive(on: DispatchQueue.main)
        .sink { completion in
            if case let .failure(error) = completion {
                self.state = .didFail(error: error.localizedDescription)
            }
        } receiveValue: { [weak self] transactionId in
            guard let self = self else { return }
                        
            self.waitForSealedTransaction(transactionId) {
                self.notes = nil
                self.state = .idle
            }
        }
        .store(in: &cancellables)
    }
    
    func queryNotes() {
        guard let currentUserAddress = fcl.currentUser?.addr else { return }
        
        state = .loading
        
        fcl.query {
            cadence {
                // Script that tries to get all notes from the 
                // current Notepad. It returns nil in case the 
                // Notepad doesn't exist yet publicly:
                """
                import NotepadManagerV1 from \(notepadManagerAddress)
                
                pub fun main(): [NotepadManagerV1.NoteDTO]? {
                    let notepadAccount = getAccount(0x\(currentUserAddress))
                
                    let notepadCapability = notepadAccount.getCapability<&NotepadManagerV1.Notepad>(/public/PublicNotepadV1)
                    let notepadReference = notepadCapability.borrow()
                
                    return notepadReference == nil ? nil : notepadReference?.allNotes()
                }
                """
            }
        }
        .receive(on: DispatchQueue.main)
        .sink { [weak self] completion in
            guard let self = self else { return }
            
            if case let .failure(error) = completion {
                self.state = .didFail(error: error.localizedDescription)
            }
        } receiveValue: { [weak self] result in
            print(result)
            
            guard let self = self else { return }
            guard let valuesArray = result.fields?.value.toOptional()?.value.toArray() else {
                self.notes = nil
                self.state = .idle
                return
            }
            
            let notes: [Note] = valuesArray.compactMap {
                guard let noteData = $0.value.toStruct()?.fields else { return nil }
                // It's not the best decoding code, but enough 
                // for what we are illustrating. The SDK 
                // maintainer is working on a better data 
                // decoder for future versions.
                let id = noteData[0].value.value.toUInt64()
                let title = noteData[1].value.value.toString()
                let body = noteData[2].value.value.toString()
                
                guard let id = id, let title = title, let body = body else { return nil }
                return Note(id: id, title: title, body: body)
            }
            
            self.notes = notes
            self.state = .idle
        }.store(in: &cancellables)
    }
    
    func currentErrorDidDismiss() {
        state = .idle
    }
    
    func signOut() {
        fcl.currentUser = nil
    }
    
    // MARK: - Private
    
    private func waitForSealedTransaction(_ transactionId: String, onSuccess: @escaping () -> Void) {
        // We are using a background thread because we don't 
        // want the .wait() to block the main event loop:
        DispatchQueue.global(qos: .userInitiated).async {
            do {
                // We want to wait for the transaction to be 
                // sealed (last transaction state) because we 
                // need the final result to be available in 
                // the blockchain. It's slower, but we don't 
                // want intermediate transaction states where 
                // you can still get empty errors or data when 
                // querying the blockchain with a Script.
                let result = try Flow.ID(hex: transactionId).onceSealed().wait()
                
                // And we update the state again in the main thread:
                DispatchQueue.main.async {
                    print(result)
                    
                    if !result.errorMessage.isEmpty {
                        self.state = .didFail(error: result.errorMessage)
                    } else {
                        onSuccess()
                    }
                }
            } catch {
                DispatchQueue.main.async {
                    self.state = .didFail(error: error.localizedDescription)
                }
            }
        }
    }
}
```

### `NoteEditorView.swift`

![NoteEditorView](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ykr14lqcdcyh73x1vq8p.png)

The `NoteEditorView` is just a form to compose and send a new note.

```swift
import SwiftUI

struct NoteEditorView: View {
    
    @EnvironmentObject private var vm: MyNotesVM
    @Environment(\.dismiss) private var dismiss

    @State private var noteTitle = ""
    @State private var noteBody = ""
    
    private var canSaveNote: Bool { !noteTitle.isEmpty && !noteBody.isEmpty}
    
    private var saveButton: some View {
        Button("Save") {
            vm.createNote(title: noteTitle, body: noteBody)
            dismiss()
        }
        .opacity(canSaveNote ? 1 : 0.5)
        .disabled(!canSaveNote)
    }
    
    var body: some View {
        NavigationView {
            VStack {
                VStack {
                    TextField("Title", text: $noteTitle)
                        .foregroundColor(.black)
                        .font(.body.weight(.bold))
                        .accentColor(.black)
                        .padding(.vertical, 7)
                    
                    TextField("Body", text: $noteBody)
                        .foregroundColor(.black)
                        .accentColor(.black)
                        .padding(.vertical, 7)
                }
                .padding(.horizontal)
                .background(UI.flowGreen.opacity(0.5))
                .cornerRadius(10)
                
                Spacer()
            }
            .padding()
            .navigationTitle("New note")
            .navigationBarTitleDisplayMode(.inline)
            .navigationBarItems(trailing: saveButton)
        }
        .tint(UI.flowGreen)
    }
}

struct NoteEditor_Previews: PreviewProvider {
    static var previews: some View {
        NoteEditorView()
            .preferredColorScheme(.dark)
            .environmentObject(MyNotesVM())
    }
}
```

### `Note.swift` 

Just the Note model:

```swift 
import Flow
import Foundation

struct Note {
    let id: UInt64
    let title: String
    let body: String
}
```

And that's it! I hope you find this helpful and that it inspires you to find more cool use cases. If you want to see a full demo video in action and check this project's source code, please refer to the [FlowNotes repository](https://github.com/jfsagasti/FlowNotes). 

There's an edit transaction use case in the `NotepadManager` contract that I haven't used in the app. I leave it to you as an exercise if you are motivated enough :)

Thank you for reading!