MangoByte MangoByte
 ·  Essay

Live Bitcoin Price Tracker in SwiftUI

To celebrate Bitcoin’s milestone in breaking $70,000 and reaching a new all-time high, let’s build a live Bitcoin price tracker in SwiftUI using a WebSocket connection. This tutorial will guide you through the process step-by-step, explaining each component along the way.

A live bitcoin price tracker implemented in SwiftUI and shown in a iOS simulator

Step 1: Define the Data Model

First, we’ll create a simple data model to map the JSON we’ll receive from the API:

struct BitcoinPriceResponse: Codable {
    var bitcoin: String?
}

This structure conforms to the Codable protocol, allowing easy decoding of the JSON response.

Step 2: Create the ViewModel

Next, we’ll create a ViewModel to handle the logic and connection to the WebSocket API. We’ll use the new @Observable macro to simplify observing data changes in our view:

@Observable
class WebSocketViewModel {
    var bitcoinPrice: String = "Loading..."
    var btcDouble: Double = 0.0
    
    private var webSocketTask: URLSessionWebSocketTask?
    private let urlSession = URLSession(configuration: .default)
    
    // ... (connect, disconnect, receiveMessage, and decodeMessage functions will be added here)
}

Let’s break down the key functions in our ViewModel:

Connect Function

func connect() {
    guard let url = URL(string: "wss://ws.coincap.io/prices?assets=bitcoin") else {
        print("Invalid URL")
        return
    }
    webSocketTask = urlSession.webSocketTask(with: url)
    webSocketTask?.resume()
    
    receiveMessage()
}

This function establishes the WebSocket connection and starts receiving messages.

Disconnect Function

func disconnect() {
    webSocketTask?.cancel(with: .goingAway, reason: nil)
}

This function cleanly disconnects the WebSocket when needed.

Receive Message Function

private func receiveMessage() {
    webSocketTask?.receive { [weak self] result in
        switch result {
            case .failure(let error):
                print("WebSocket receive error: \(error)")
            case .success(.string(let str)):
                self?.decodeMessage(jsonString: str)
                self?.receiveMessage() // Continue listening
            default:
                break
        }
    }
}

This function handles incoming messages from the WebSocket and continues listening for new messages.

Decode Message Function

private func decodeMessage(jsonString: String) {
    guard let data = jsonString.data(using: .utf8) else { return }
    do {
        let response = try JSONDecoder().decode(BitcoinPriceResponse.self, from: data)
        if let bitcoinPrice = response.bitcoin {
            DispatchQueue.main.async {
                self.bitcoinPrice = "\(bitcoinPrice)"
                self.btcDouble = bitcoinPrice.toDouble() ?? 0.0
            }
        }
    } catch {
        print("JSON Decoding error: \(error)")
    }
}

This function decodes the JSON response and updates the ViewModel’s properties with the new Bitcoin price.

Step 3: Create the SwiftUI View

Now, let’s create a SwiftUI view to display the Bitcoin price:

struct BitcoinPriceView: View {
    var viewModel = WebSocketViewModel()
    
    var currencyFormatter: NumberFormatter = {
        var formatter = NumberFormatter()
        formatter.maximumFractionDigits = .zero
        formatter.numberStyle = .currency
        formatter.currencySymbol = "$"
        return formatter
    }()
    
    var body: some View {
        VStack {
            Image(systemName: "bitcoinsign")
                .font(.caption2)
                .glow()
            Text("\(currencyFormatter.string(from: viewModel.btcDouble as NSNumber)!)")
                .font(.largeTitle)
                .monospacedDigit()
                .transaction { transaction in
                    transaction.animation = .default
                }
                .contentTransition(.numericText(value: viewModel.btcDouble))
                .glow()
        }
        .fontDesign(.monospaced)
        .onAppear {
            viewModel.connect()
        }
        .onDisappear {
            viewModel.disconnect()
        }
    }
}

This view uses the ViewModel to display the current Bitcoin price. It also applies some styling and animations to make the display more engaging.

Step 4: Add Helper Extensions

To enhance our view, we’ll add two helper extensions:

String Extension

extension String {
    func toDouble() -> Double? {
        return NumberFormatter().number(from: self)?.doubleValue
    }
}

This extension helps convert String values to Double.

View Extension

extension View {
    func glow() -> some View {
        self
            .foregroundColor(.white)
            .background {
                self
                    .shadow(color: .yellow, radius: 4)
            }
            .background {
                self
                    .shadow(color: .orange, radius: 2)
            }
            .background {
                self
                    .shadow(color: .red, radius: 12)
            }
    }
}

This extension adds a glow effect to any view, enhancing the visual appeal of our Bitcoin price display.

Conclusion

We’ve created a live Bitcoin price tracker using SwiftUI and WebSocket. The project demonstrates how to handle real-time data updates, use the new @Observable macro, and apply custom styling and animations in SwiftUI.

Feel free to copy the code and explore other techniques that you might find interesting!

Full Code

Here’s the complete code for the Bitcoin Price Tracker:

import SwiftUI
import Foundation// Define a structure for the JSON response
struct BitcoinPriceResponse: Codable {
    var bitcoin: String?
}
// ViewModel that handles WebSocket connection
@Observable
class WebSocketViewModel {
    var bitcoinPrice: String = "Loading..."
    var btcDouble: Double = 0.0
    
    private var webSocketTask: URLSessionWebSocketTask?
    private let urlSession = URLSession(configuration: .default)
    
    func connect() {
        guard let url = URL(string: "wss://ws.coincap.io/prices?assets=bitcoin") else {
            print("Invalid URL")
            return
        }
        webSocketTask = urlSession.webSocketTask(with: url)
        webSocketTask?.resume()
        
        receiveMessage()
    }
    
    func disconnect() {
        webSocketTask?.cancel(with: .goingAway, reason: nil)
    }
    
    private func receiveMessage() {
        webSocketTask?.receive { [weak self] result in
            switch result {
                case .failure(let error):
                    print("WebSocket receive error: \(error)")
                case .success(.string(let str)):
                    self?.decodeMessage(jsonString: str)
                    self?.receiveMessage() // Continue listening
                default:
                    break
            }
        }
    }
    
    private func decodeMessage(jsonString: String) {
        guard let data = jsonString.data(using: .utf8) else { return }
        do {
            let response = try JSONDecoder().decode(BitcoinPriceResponse.self, from: data)
            if let bitcoinPrice = response.bitcoin {
                DispatchQueue.main.async {
                    self.bitcoinPrice = "\(bitcoinPrice)"
                    self.btcDouble = bitcoinPrice.toDouble() ?? 0.0
                }
            }
        } catch {
            print("JSON Decoding error: \(error)")
        }
    }
    
    deinit {
        disconnect()
    }
}
// SwiftUI View
struct BitcoinPriceView: View {
    
    var viewModel = WebSocketViewModel()
    
    var currencyFormatter: NumberFormatter = {
        var formatter = NumberFormatter()
        formatter.maximumFractionDigits = .zero
        formatter.numberStyle = .currency
        formatter.currencySymbol = "$"
        
        return formatter
    }()
    
    var body: some View {
        VStack {
            Image(systemName: "bitcoinsign")
                .font(.caption2)
                .glow()
            Text("\(currencyFormatter.string(from: viewModel.btcDouble as NSNumber)!)")
                .font(.largeTitle)
                .monospacedDigit()
                .transaction { transaction in
                    transaction.animation = .default
                }
                .contentTransition(.numericText(value: viewModel.btcDouble))
                .glow()
        }
        .fontDesign(.monospaced)
        .onAppear {
            viewModel.connect()
        }
        .onDisappear {
            viewModel.disconnect()
        }
    }
}
// Preview for SwiftUI Canvas
#Preview {
    NavigationStack {
        BitcoinPriceView()
    }
    .preferredColorScheme(.dark)
}
extension String {
    func toDouble() -> Double? {
        return NumberFormatter().number(from: self)?.doubleValue
    }
}
extension View {
    func glow() -> some View {
        self
            .foregroundColor(.white)
            .background {
                self
                    .shadow(color: .yellow, radius: 4)
            }
            .background {
                self
                    .shadow(color: .orange, radius: 2)
            }
            .background {
                self
                    .shadow(color: .red, radius: 12)
            }
    }
}
All notes and essays
© 2026 MangoByte. Built simple, on purpose.