11 min read
Getting Started with Swift and BDK

When deciding how to build a mobile application, a consequential consideration is whether to pick a cross-platform or native solution. Small development groups frequently reach for frameworks like Flutter or React Native, as both platforms allow for quick prototyping, releases and updates. In this article I am going to make the case for building a native mobile application in Swift, what benefits this development approach offers both your team of developers and your end-users, and how to transition to Swift with experience in React Native.

How Rust is Changing Development

Before talking about Swift, it is important to note how Rust is changing the workflow for development teams in a great way. Since the Mozilla team has launched UniFFI, the viability of building Swift and Kotlin apps in small teams has increased significantly. BDK is a great example of how UniFFI can concentrate the core business-related logic into a single Rust codebase, and allow app developers to focus on the experience they are delivering their users. Rust and UniFFI allow for a greater specialization of small teams, with core engineers focused on Rust, and mobile engineers specializing in the user experience on iOS or Android platforms. To read more on UniFFI and the approach BDK takes when building UniFFI libraries, check out this article on the BDK blog.

The Advantages of Building Natively

Brace yourself for some opinionated anecdotes. I have experience building mobile applications in both React Native and Swift, and I prefer my developer experience building an iOS app directly with Swift. Different teams will have different opinions, but this is what I have found when comparing the experience between the two platforms.

To me, native applications look better. By using SwiftUI, padding, fonts, buttons, and other user interface objects are directly accessible to developers, often usable in applications with no modifications. There are hundreds of prebuilt user interface components that are polished and responsive with no additional imports. Animations that users are accustomed to in other apps, but may not notice directly, are incorporated as first-class support when building on Swift and SwiftUI.

As a more general philosophy when building applications that handle money, the user should and deserves to feel comfortable while using the application. Swift and SwiftUI helps small development teams achieve a friendly, professional, and effective user interface without having to form the building blocks from scratch. When targeting specific platforms, users get the experience of a product that was built by a large engineering team, yet it might be built by only a few bitcoin developers with a keen eye for design.

Authentication, whether biometrics or passcodes, secret storage, cryptographic primitives, and many more APIs are directly available to native developers. In the case of Swift, the team at Apple maintains these APIs directly, and integration into Swift apps can be as little as a few lines of code.

Although I find it quite annoying that Apple gate-keeps the ability to build iOS apps behind Apple computers, their tooling for building apps is powerful. Xcode displays live previews of your app screens, so you can tune the finest details and iterate quickly. There are simulators for each iOS device, and testing the logic of your application is robust.

Enough talking about Swift, the rest of the article will be focused on how to get up and running with BDK for those that have no prior iOS programming knowledge, but potentially a background in React Native. We will have a look at a few snippets of code from Matthew Ramsden’s Example Swift Wallet in the BDK collection of repositories.

Requirements

To follow along using Matthew’s Github project, you will need an Apple computer running macOS Sonoma 14.0 or later and Xcode 15.0 or later. To install Xcode if you do not have it already, you may find Xcode on the App Store. Next clone the repository:

  • git clone https://github.com/bitcoindevkit/BDKSwiftExampleWallet.git

Now open Xcode in the folder you just cloned.

SwiftUI Components

Let’s have a look at a screen that comes up frequently in bitcoin applications: the screen to receive bitcoin, found at BDKSwiftExampleWallet/View/ReceiveView.swift

import BitcoinUI
import SwiftUI

struct ReceiveView: View {
    @Bindable var viewModel: ReceiveViewModel
    @State private var isCopied = false
    @State private var showCheckmark = false

	var body: some View {
		...
	}

We start by importing the necessary libraries at the top of our file, and then we define a struct that extends View. A View only has one requirement, that it contains a body. For those coming from React Native, this concept should feel quite familiar. Indeed, we often use the <View></View> JSX tag in a React Native application to define sections of the screen. The outer-most SwiftUI View is where you define what the user sees in your application.

How about those property wrappers, @Bindable and @State? Staring with @State, we are telling SwiftUI to keep track of the variable we define, and update the user interface when changes to that variable occur. More familiarity to React Native! This definition maps directly to the useState hook we use so frequently in RN. And @Bindable? @Bindable is similar to @State, except the ReceiveViewModel publishes changes that @Bindablelistens for, and the viewModel defined as a variable may also call methods in the ReceiveViewModel to carry out business logic. This principle of separating the application logic from the application screens is known as the Model-View-ViewModel architecture. We will explore a ViewModel in the next section.

What makes SwiftUI user interface components is the ability to combine the appearance and behavior of a component with modifiers. To see how to modify the appearance of a component in our view, let’s observe how we display the address to the user.

VStack(spacing: 8) {
	Image(systemName: "bitcoinsign.circle.fill")
		.resizable()
		.foregroundColor(.bitcoinOrange)
		.fontWeight(.bold)
		.frame(width: 50, height: 50, alignment: .center)

	Text("Receive Address")
		.fontWeight(.semibold)
}
.font(.caption)
.padding(.top, 40.0)

While it might look intimidating at first, the modifier pattern of SwiftUI is my favorite feature. There are no style sheets or CSS tags, we simply add the appearance we would like to modify by chaining modifiers on a View component.

Similarly, we can define behavior on SwiftUI components too. The button to copy the address of the user demonstrates this well:

Button {
	UIPasteboard.general.string = viewModel.address
	isCopied = true
	showCheckmark = true
	DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
		isCopied = false
		showCheckmark = false
	}
} label: {
	HStack {
		withAnimation {
			Image(systemName: showCheckmark ? "checkmark" : "doc.on.doc")
		}
	}
	.fontWeight(.semibold)
	.foregroundColor(.bitcoinOrange)
}

This Button takes a function closure that can define arbitrary behavior when the user interacts with the button. Here, we copy the address to the clipboard and animate a checkmark on the screen. The power of SwiftUI is on display here, as we animated an icon, copied information to the user clipboard, and styled a button with less than 15 lines of code. All with zero external dependencies.

Defining Behavior with ViewModels

We saw earlier that we can have SwiftUI listen for changes on our viewModel variable using the @Bindable, and we can use this viewModel to execute logic for our View. Now we turn to the ViewModel defined in BDKSwiftExampleWallet/View Model/ReceiveViewModel.swift

import BitcoinDevKit
import Foundation
import Observation

@Observable
class ReceiveViewModel {
    let bdkClient: BDKClient
	var address: String = ""
	var receiveViewError: BdkError?
    var showingReceiveViewErrorAlert = false
    
    init(bdkClient: BDKClient = .live) {
		self.bdkClient = bdkClient
	}
	...
}

We import our necessary libraries as usual, and wrap the ReceiveViewModel class in the @Observable property wrapper. Now, in our View, we may call the associated methods in ReceiveViewModel and listen for changes on those variables. The only method in the ReceiveViewModel is a function to getAddress to receive bitcoin.

func getAddress() {
	do {
		let address = try bdkClient.getAddress()
		self.address = address
	} catch let error as WalletError {
		self.receiveViewError = .Generic(message: error.localizedDescription)
		self.showingReceiveViewErrorAlert = true
	} catch let error as BdkError {
		self.receiveViewError = .Generic(message: error.description)
		self.showingReceiveViewErrorAlert = true
	} catch {
		self.receiveViewError = .Generic(message: "Error Getting Address")
		self.showingReceiveViewErrorAlert = true
	}
}

We will talk about the bdkClient variable in the next section, but for now we know that it encapsulates our wallet logic in a single structure. After taking a look at this error handling, it is easy to see why we break out the core functionality into a new file. The conept of a ViewModelis similar to hooks in React Native, however I find the ViewModel approach far simpler. We define a structure with some behavior for our users, and allow our View to invoke functions and listen for changes on the variables in the ViewModel.

The Wallet

Finally, the part you’ve been waiting for. How do we use BDK? The client for interacting with BDK in the app is found at BDKSwiftExampleWallet/Service/BDK Service/BDKService.swift

import BitcoinDevKit
import Foundation

private class BDKService {
    static var shared: BDKService = BDKService()
    private var balance: Balance?
    private var blockchainConfig: BlockchainConfig?
    var network: Network
    private var wallet: Wallet?
    private let keyService: KeyClient
	...

The BDKService contains all of our wallet related functionality in a structure known as a singleton. The singleton pattern is used for defining a class that should only be used once throughout the application, which explains the static variable that defines a BDKService within the class itself. The semantics of singletons are not too important for our application, but we see that some important global variables are defined in the BDKService, such as the balance of the wallet or our chain source, blockchainConfig. Initializing the blockchainConfig is simple.

init(
	keyService: KeyClient = .live
) {
	let storedNetworkString = try! keyService.getNetwork() ?? Network.testnet.description
	let storedEsploraURL =
		try! keyService.getEsploraURL()
		?? Constants.Config.EsploraServerURLNetwork.Testnet.mempoolspace
	self.network = Network(stringValue: storedNetworkString) ?? .testnet
	self.keyService = keyService
	let esploraConfig = EsploraConfig(
		baseUrl: storedEsploraURL,
		proxy: nil,
		concurrency: nil,
		stopGap: UInt64(20),
		timeout: nil
	)
	let blockchainConfig = BlockchainConfig.esplora(config: esploraConfig)
	self.blockchainConfig = blockchainConfig
}

Ignoring the keyService for a second, we see how to define the chain source to an Esplora client. Next we can load the wallet stored in the iOS secure storage and set the wallet within the BDKService.

private func loadWallet(descriptor: Descriptor, changeDescriptor: Descriptor) throws {
	let wallet = try Wallet(
		descriptor: descriptor,
		changeDescriptor: changeDescriptor,
		network: network,
		databaseConfig: .memory
	)
	self.wallet = wallet
}

Now we should update our local wallet database with the Esplora client. We do this to ensure we do not reuse a user’s address.

func sync() async throws {
	guard let config = self.blockchainConfig else {
		throw WalletError.blockchainConfigNotFound
	}
	guard let wallet = self.wallet else {
		throw WalletError.walletNotFound
	}
	let blockchain = try Blockchain(config: config)
	try wallet.sync(blockchain: blockchain, progress: nil)
}

The majority of this function is ensuring our configuration and wallet have been properly defined. All it takes to sync our local database to the chain is a fallible call to a wallet method sync.

When the user requests their new address to receive bitcoin, we simply call a method on self.wallet.

func getAddress() throws -> String {
	guard let wallet = self.wallet else {
		throw WalletError.walletNotFound
	}
	let addressInfo = try wallet.getAddress(addressIndex: .new)
	return addressInfo.address.asString()
}

That is all there is to it. Make sure to explore the rest of the file for more BDK related logic! At this point you may be wondering, we defined the BDKService, but where is the BDKClient used in the ViewModel? We define a BDKClient that exposed methods on the static var shared: BDKService = BDKService() defined in our BDKService. This pattern allows for mock testing and single instantiations of a BDKService.

Closing Remark

BDK demonstrates how Rust can be used to separate the core logic of an application from the user interface. Part of what makes BDK so powerful is the ability to build bitcoin applications in a wide variety of languages with uncompromising functionality. To deliver the optimal experience for our users, it is important to consider the foundations we set early in our development. When you choose BDK as part of the core of an application, you can concentrate on what the end user sees, instead of the wallet functionality. In my experience, Swift and SwiftUI are the best tools to deliver an iOS experience to users, and it can a great development experience too.