From Architecture to Reality: Building Real-Time BTC Price Apps
Connecting all layers with Composition Root, ViewModels, and real apps. How macOS App Sandbox almost killed our network calls, and why CLI apps get special privileges.
Introduction
In the previous article we built a persistence layer with UserDefaults, benchmarked three solutions, and discovered SwiftDataβs Decimal precision bug.
Now we have:
- Networking layer (Binance + CryptoCompare fallback)
- Persistence layer (UserDefaults)
- Domain use cases (fetch, persist, render)
- All tested, all modular
But theyβre disconnected. We need to wire everything together and build actual apps.
The challenge: Turn isolated modules into a real-time BTC price monitor.
This article covers the final mile:
- Composition Root - Wiring dependencies without coupling
- ViewModel - Connecting use cases to SwiftUI with @Observable
- SwiftUI App - Real-time UI with automatic updates
- CLI Tool - Terminal app for developers
- The Sandbox Crisis - How macOS security almost broke everything
The surprise: Building the apps took 50 lines of code. Debugging the sandbox took 2 hours.
By the end, youβll see why Composition Root matters, how @Observable simplifies state management, and why macOS apps need explicit network permissions while CLI tools donβt.
Step 1: The Composition Root - Dependency Injection Done Right
Problem: Our modules are isolated. How do we connect them without creating tight coupling?
Wrong approach:
// β Don't do this - ViewModels shouldn't know infrastructureclass BTCPriceViewModel { let loader = BinancePriceLoader(session: .shared) // Tight coupling let store = UserDefaultsPriceStore() // Can't test}Clean Architecture principle: High-level modules shouldnβt depend on low-level modules.
Solution: Composition Root pattern.
What is Composition Root?
A single place where we:
- Create all concrete implementations
- Wire dependencies together
- Inject them into use cases
Key insight: Composition happens once at app startup, not scattered throughout code.
Creating BTCPriceComposer Module
btc-price/βββ BTCPriceCore/ # Domain (protocols, use cases)βββ BTCPriceNetworking/ # Infrastructureβββ BTCPricePersistence/ # Infrastructureβββ BTCPriceComposer/ # Composition Root (new)Why separate module?
- β Centralizes dependency creation
- β Apps import only composer, not individual infrastructure
- β Makes dependency graph explicit
- β Easy to swap implementations (tests, previews)
Implementation: AppDependencies
import BTCPriceCoreimport BTCPriceNetworkingimport BTCPricePersistenceimport Foundation
public final class AppDependencies: Sendable { // Store public let priceStore: PriceStore
// Loaders public let primaryLoader: PriceLoader public let fallbackLoader: PriceLoader
// Use Cases public let fetchWithFallback: FetchWithFallback public let persistPrice: PersistLastValidPrice public let renderPrice: RenderPriceAndTimestamp
public init( userDefaults: UserDefaults = .standard, urlSession: URLSession = .shared ) { // 1. Create infrastructure self.priceStore = UserDefaultsPriceStore( userDefaults: userDefaults, key: "btc_price_cache" )
self.primaryLoader = BinancePriceLoader(session: urlSession) self.fallbackLoader = CryptoComparePriceLoader(session: urlSession)
// 2. Wire use cases self.fetchWithFallback = FetchWithFallback( primary: primaryLoader, fallback: fallbackLoader )
self.persistPrice = PersistLastValidPrice(store: priceStore) self.renderPrice = RenderPriceAndTimestamp( priceFormatter: USDPriceFormatter(), timestampFormatter: ISO8601TimestampFormatter() ) }}Design Decisions
Why Sendable?
- Swift 6 concurrency requirement
- Can be safely shared across tasks/actors
Why inject UserDefaults and URLSession?
- Testing: Can inject custom suite and mocked session
- Flexibility: Different configurations for production/debug
Why expose both infrastructure and use cases?
- Use cases: For app logic (ViewModel uses these)
- Infrastructure: For direct access if needed (rare)
Why final class?
- Not meant to be subclassed
- Composition over inheritance
Dependency Graph
AppDependenciesβββ priceStore: UserDefaultsPriceStoreβ βββ UserDefaultsβββ primaryLoader: BinancePriceLoaderβ βββ URLSessionβββ fallbackLoader: CryptoComparePriceLoaderβ βββ URLSessionβββ fetchWithFallback: FetchWithFallbackβ βββ primary: BinancePriceLoaderβ βββ fallback: CryptoComparePriceLoaderβββ persistPrice: PersistLastValidPriceβ βββ store: UserDefaultsPriceStoreβββ renderPrice: RenderPriceAndTimestamp βββ priceFormatter: USDPriceFormatter βββ timestampFormatter: ISO8601TimestampFormatterClean Architecture win: All dependencies point inward to domain.
Step 2: The CLI Tool - Simplicity First
Before building the complex SwiftUI app, letβs validate with a simple CLI tool.
Goal: Fetch BTC price every second, print to terminal.
Implementation: main.swift
import BTCPriceCoreimport BTCPriceComposerimport Foundation
let deps = AppDependencies()
print("π Starting BTC/USD Price Monitor")print("π Updates every second. Press CTRL+C to stop.")print("==========================================")print("")
var updateCount = 0
while true { updateCount += 1
do { // 1. Fetch price let quote = try await deps.fetchWithFallback.execute()
// 2. Persist for offline support try await deps.persistPrice.execute(quote)
// 3. Render formatted output let formatted = await deps.renderPrice.execute(quote)
print("[\(updateCount)] π° \(formatted.priceText) | π \(formatted.timestampText)")
} catch { // 4. Fallback to cache if network fails if let cached = await deps.persistPrice.loadCached() { let formatted = await deps.renderPrice.execute(cached) print("[\(updateCount)] π¦ [CACHED] \(formatted.priceText) | π \(formatted.timestampText)") } else { print("[\(updateCount)] β Error: \(error)") } }
// 5. Wait 1 second before next update try? await Task.sleep(for: .seconds(1))}Key Features
- Real dependencies: Uses AppDependencies() - no mocks
- Error resilience: Falls back to cache when network fails
- Continuous updates: Infinite loop with 1-second delay
- Progress tracking: Shows update count
- Graceful degradation: Shows cached data instead of crashing
Running the CLI
$ swift run BTCPrice-CLI
π Starting BTC/USD Price Monitorπ Updates every second. Press CTRL+C to stop.==========================================
[1] π° $114,459.80 | π Oct 27, 2025 at 7:56:39 PM[2] π° $114,461.23 | π Oct 27, 2025 at 7:56:40 PM[3] π° $114,458.91 | π Oct 27, 2025 at 7:56:41 PM...It just works. No configuration, no entitlements, no sandbox issues.
(Weβll discover why later - CLI tools have special privileges.)
Step 3: The ViewModel - Connecting Use Cases to SwiftUI
Now the interesting part: building a reactive ViewModel for SwiftUI.
Requirements:
- Fetch price every second automatically
- Update UI when new data arrives
- Show cached data when offline
- Display loading/error states
- Clean up resources when view disappears
Challenge: State Management in Swift 6
Old approach (pre-Swift 6):
class BTCPriceViewModel: ObservableObject { @Published var priceText: String = "--" // Manual @Published wrappers @Published var isLoading: Bool = false}New approach (Swift 6):
@Observablefinal class BTCPriceViewModel { var priceText: String = "--" // Automatic observation var isLoading: Bool = false}@Observable benefits:
- β No @Published boilerplate
- β Automatic observation of ALL properties
- β Better performance (fine-grained updates)
- β Cleaner syntax
Implementation: BTCPriceViewModel
import BTCPriceCoreimport BTCPriceComposerimport Foundation
@Observablefinal class BTCPriceViewModel { // MARK: - Observable State var priceText: String = "--" var timestampText: String = "--" var isLoading: Bool = false var errorMessage: String? var isUsingCache: Bool = false
// MARK: - Dependencies private let dependencies: AppDependencies private var updateTask: Task<Void, Never>?
init(dependencies: AppDependencies = AppDependencies()) { self.dependencies = dependencies }
// MARK: - Public API
func startMonitoring() { guard updateTask == nil else { return } // Prevent multiple tasks
updateTask = Task { while !Task.isCancelled { await fetchPrice() try? await Task.sleep(for: .seconds(1)) } } }
func stopMonitoring() { updateTask?.cancel() updateTask = nil }
func refresh() async { await fetchPrice() }
// MARK: - Private Helpers
private func fetchPrice() async { isLoading = true errorMessage = nil isUsingCache = false
do { // 1. Fetch fresh price let quote = try await dependencies.fetchWithFallback.execute()
// 2. Save to cache try await dependencies.persistPrice.execute(quote)
// 3. Render formatted text let formatted = await dependencies.renderPrice.execute(quote)
// 4. Update UI priceText = formatted.priceText timestampText = formatted.timestampText isLoading = false
} catch { // 5. Fallback to cache if let cached = await dependencies.persistPrice.loadCached() { let formatted = await dependencies.renderPrice.execute(cached) priceText = formatted.priceText timestampText = formatted.timestampText isUsingCache = true } else { errorMessage = "Unable to load price" }
isLoading = false } }}Design Decisions
Why Task instead of Timer?
- Modern concurrency with async/await
- Easy cancellation (Task.cancel())
- Better resource management
- Works with actors naturally
Why guard updateTask == nil?
- Prevents duplicate tasks if startMonitoring() called twice
- Resource leak protection
Why separate fetchPrice() method?
- Single responsibility: one method = one fetch
- Reusable for manual refresh
- Easier to test (can call directly)
Why isUsingCache flag?
- UI can show βoffline modeβ indicator
- User knows data might be stale
Why @Observable instead of @ObservableObject?
- Less boilerplate (no @Published)
- Better performance (fine-grained observation)
- Modern Swift pattern (iOS 17+)
Error Handling Strategy
// If network fails:catch { // 1. Try cache first if let cached = await dependencies.persistPrice.loadCached() { // Show cached data with indicator isUsingCache = true } else { // 2. Only show error if no cache exists errorMessage = "Unable to load price" }}Graceful degradation: Always prefer showing stale data over error message.
Step 4: The SwiftUI App - Minimal View Code
With ViewModel handling all logic, the view is trivial:
import SwiftUI
struct ContentView: View { @State private var viewModel = BTCPriceViewModel()
var body: some View { Text(viewModel.priceText) .onAppear { viewModel.startMonitoring() } .onDisappear { viewModel.stopMonitoring() } }}Thatβs it. 14 lines for a real-time updating app.
Why So Simple?
- @State: Creates observable instance
- .onAppear: Starts monitoring when view appears
- .onDisappear: Stops monitoring when view disappears (resource cleanup)
- viewModel.priceText: Automatic UI updates when property changes
The App Entry Point
import SwiftUI
@mainstruct BTCPriceAppApp: App { var body: some Scene { WindowGroup { ContentView() } }}Standard SwiftUI app structure. Nothing special needed.
Step 5: The Sandbox Crisis - When Everything Breaks
Expected: Run app, see price updates.
Reality: App shows β forever.
The Console Output π¨
networkd_settings_read_from_file Sandbox is preventing this processfrom reading networkd settings file at"/Library/Preferences/com.apple.networkd.plist", please add an exception.
nw_resolver_create_dns_service_locked [C1.1]DNSServiceCreateDelegateConnection failed: ServiceNotRunning(-65563)
Connection 1: failed to connect 10:-72000, reason -1
Task <...> HTTP load failed, 0/0 bytes (error code: -1003 [10:-72000])
Error Domain=NSURLErrorDomain Code=-1003"A server with the specified hostname could not be found."Translation: macOS App Sandbox is blocking all network access.
The Mystery: Why Does CLI Work But App Doesnβt?
CLI tool: Works perfectly, fetches prices every second. macOS app: Canβt even resolve DNS.
Investigation:
# CLI runs without sandbox$ swift run BTCPrice-CLIβ
Works - fetches from api.binance.com
# macOS app runs WITH sandbox$ open BTCPriceApp.appβ Fails - sandbox blocks networkUnderstanding macOS App Sandbox
What is it?
- Security feature that restricts app capabilities
- Enabled by default for macOS apps distributed on App Store
- Prevents unauthorized access to:
- Network
- File system outside container
- User data
- System resources
Why doesnβt CLI have sandbox?
- Command-line tools are not sandboxed by default
- They run with userβs full permissions
- Not distributed through App Store
Key insight: Security vs convenience trade-off.
The Solution: Network Entitlements
Entitlements = explicit permission declarations for sandboxed apps.
To fix network access:
- Open Xcode
- Select BTCPriceApp target (NOT CLI)
- Go to βSigning & Capabilitiesβ tab
- Click β+ Capabilityβ
- Add βApp Sandboxβ (if not already present)
- Enable: β Outgoing Connections (Client)
This creates an entitlements file:
<!-- BTCPriceApp.entitlements --><?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN""http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict> <key>com.apple.security.app-sandbox</key> <true/> <key>com.apple.security.network.client</key> <true/></dict></plist>What This Does
com.apple.security.app-sandbox: Enables sandboxcom.apple.security.network.client: Allows outgoing network connections
Security note: Still restricted from:
- β Incoming connections (server mode)
- β Arbitrary file access
- β Reading other appsβ data
After adding entitlement:
# Rebuild and runβ
App now fetches prices successfullyβ
DNS resolution worksβ
HTTPS connections succeedDebugging Tips We Learned
- Check Console.app: macOS logs sandbox violations
- Look for βSandbox is preventingβ: Keyword for sandbox issues
- Compare targets: If one works and another doesnβt, check entitlements
- Read error codes: -1003 = βCould not find serverβ often means DNS blocked
Step 6: The Final UI - Beyond Plain Text
After fixing sandbox, we enhanced the UI:
struct ContentView: View { @State private var viewModel = BTCPriceViewModel()
var body: some View { VStack(spacing: 24) { // Bitcoin Icon Image(systemName: "bitcoinsign.circle.fill") .font(.system(size: 60)) .foregroundStyle(.orange)
// Price VStack(spacing: 8) { Text(viewModel.priceText) .font(.system(size: 48, weight: .bold)) .monospacedDigit()
HStack { Image(systemName: "clock") Text(viewModel.timestampText) } .font(.subheadline) .foregroundStyle(.secondary) }
// Live Updates Info GroupBox("Live Updates") { VStack(alignment: .leading, spacing: 12) { InfoRow( icon: "arrow.triangle.2.circlepath", label: "Update Frequency", value: "Every second" )
InfoRow( icon: "network", label: "Data Source", value: "Binance API" )
InfoRow( icon: "exclamationmark.triangle", label: "Fallback", value: "CryptoCompare" )
InfoRow( icon: "archivebox", label: "Offline Support", value: "Cached locally" ) } } } .padding() .frame(width: 400, height: 500) .onAppear { viewModel.startMonitoring() } .onDisappear { viewModel.stopMonitoring() } }}
struct InfoRow: View { let icon: String let label: String let value: String
var body: some View { HStack { Image(systemName: icon) .foregroundStyle(.blue) .frame(width: 20)
VStack(alignment: .leading, spacing: 2) { Text(label) .font(.caption) .foregroundStyle(.secondary) Text(value) .font(.subheadline.weight(.medium)) }
Spacer() } }}Result: Professional-looking app with:
- Bitcoin icon
- Large price display with monospaced digits
- Timestamp
- Feature list (update frequency, data source, fallback, offline support)
Real Development Challenges We Solved
Challenge 1: βApp Shows β Forever, No Error Messagesβ
Problem: App launches but never updates price.
Symptoms:
- No obvious errors in Xcode console
- CLI works fine
- SwiftUI view appears normal
Investigation:
- Checked Console.app (macOS system logs)
- Found: βSandbox is preventing network accessβ
Root cause: macOS App Sandbox blocks network by default.
Solution: Add com.apple.security.network.client entitlement.
Lesson: Check Console.app for sandbox violations. Xcode doesnβt always show them.
Challenge 2: βCLI and App Behave Differentlyβ
Problem: Same code works in CLI, fails in app.
Why:
- CLI tools: Not sandboxed, full user permissions
- macOS apps: Sandboxed by default, restricted capabilities
Solution: Understand platform differences, configure appropriately.
Lesson: Donβt assume all Swift executables have same capabilities.
Challenge 3: βWhen to Use @Observable vs @ObservableObjectβ
Problem: SwiftUI has two observation patterns, which to use?
Decision:
- @Observable (iOS 17+): Modern, less boilerplate, better performance
- @ObservableObject (iOS 13+): Legacy, more compatible, requires @Published
Our choice: @Observable (targeting iOS 17+)
Lesson: Modern patterns are simpler, but check platform requirements.
Challenge 4: βHow to Stop Background Tasks on View Disappearβ
Problem: ViewModel keeps fetching prices even when view is gone.
Symptoms:
- Memory leaks
- Unnecessary network calls
- Battery drain
Solution:
.onDisappear { viewModel.stopMonitoring() // Cancel the Task}Lesson: Always clean up resources in .onDisappear.
Architecture Insights
Clean Architecture Payoff - Final Validation
Look at how dependencies flow:
BTCPriceApp (Presentation) β importsBTCPriceComposer (Composition Root) β importsBTCPriceCore (Domain + Use Cases) β implemented byBTCPriceNetworking (Infrastructure)BTCPricePersistence (Infrastructure)Key win: ViewModel only knows about:
- AppDependencies (composition root)
- Domain types (PriceQuote)
- Use case protocols
ViewModel does NOT know:
- β Binance/CryptoCompare APIs
- β UserDefaults
- β JSON encoding/decoding
- β URLSession
Result: We can swap implementations without touching ViewModel.
Example: Switching to CoreData
// In AppDependencies onlypublic init(...) { // self.priceStore = UserDefaultsPriceStore(...) // Old self.priceStore = CoreDataPriceStore(...) // New
// Everything else unchanged // ViewModel doesn't need to know}Clean Architecture promise delivered: Infrastructure changes donβt propagate to business logic.
Composition Root Pattern - Why It Matters
Before Composition Root:
// ViewModel would need to know:let session = URLSession.sharedlet binance = BinancePriceLoader(session: session)let crypto = CryptoComparePriceLoader(session: session)let fetchUseCase = FetchWithFallback(primary: binance, fallback: crypto)// ... repeat in every fileWith Composition Root:
// ViewModel only needs:let deps = AppDependencies()Benefits:
- Single source of truth for dependencies
- Easy testing - inject test dependencies
- Reusable across CLI, app, previews
- Changes in one place - update AppDependencies, all consumers updated
Example - SwiftUI Preview:
#Preview { let testDeps = AppDependencies( userDefaults: .init(suiteName: "preview")!, urlSession: .mocked // Hypothetical mock ) let viewModel = BTCPriceViewModel(dependencies: testDeps) return ContentView(viewModel: viewModel)}Modern Swift Patterns Applied
- @Observable (Swift 6 / iOS 17+)
- Replaces @Published boilerplate
- Automatic observation of all properties
- Better performance
- Structured Concurrency
- Task { } instead of DispatchQueue
- Automatic cancellation with .cancel()
- async/await throughout
- Sendable Conformance
- AppDependencies: Sendable
- Safe sharing across concurrency domains
- Compiler-enforced thread safety
- Actor Isolation (in stores)
- actor UserDefaultsPriceStore
- Automatic thread safety
- No manual locks needed
Key Design Decisions We Made
1. Why Separate Composer Module?
Alternative: Put AppDependencies in app target.
Choice: Dedicated module for composition root.
Reasons:
- Reusable across CLI and App targets
- Makes dependency graph explicit
- Clear separation of concerns
- Easy to test in isolation
Trade-off: Extra module complexity vs code organization.
2. Why @Observable Instead of @ObservableObject?
Alternative: Use legacy @ObservableObject + @Published.
Choice: Modern @Observable macro.
Reasons:
- Less boilerplate (no @Published on every property)
- Better performance (fine-grained observation)
- Future-proof (SwiftUI direction)
Trade-off: iOS 17+ requirement vs better DX.
3. Why Task Instead of Timer?
Alternative: Timer.scheduledTimer(β¦) (old pattern).
Choice: Task with while loop + sleep.
Reasons:
- Works naturally with async/await
- Easy cancellation
- No retain cycles
- Cleaner code
Comparison:
// Old wayvar timer: Timer?timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in Task { await fetchPrice() } // Bridging async in sync}
// New wayupdateTask = Task { while !Task.isCancelled { await fetchPrice() // Already async try? await Task.sleep(for: .seconds(1)) }}4. Why Graceful Degradation Instead of Error Display?
Alternative: Show error message when network fails.
Choice: Fallback to cached data silently (with indicator).
Reasons:
- User experience: stale data > no data
- Offline scenarios common (airplane mode, tunnels)
- Reduce user anxiety
Implementation:
catch { if let cached = await dependencies.persistPrice.loadCached() { // Show cached with "offline" indicator isUsingCache = true } else { // Only show error if truly no data errorMessage = "Unable to load price" }}Production-Ready Results
We now have two fully functional apps:
CLI Tool
- β Runs in terminal
- β Updates every second
- β Shows update count
- β Formatted price + timestamp
- β Offline fallback
- β No configuration needed
macOS App
- β Real-time SwiftUI UI
- β Automatic updates
- β Bitcoin icon + formatted price
- β Feature information display
- β Offline support with indicator
- β Proper resource cleanup
Both apps:
- Use same composition root
- Share all business logic
- Require zero duplication
- Tested infrastructure underneath
Code Metrics
Application Code:
| Component | Lines of Code |
|---|---|
| AppDependencies | 49 LOC |
| BTCPriceViewModel | 76 LOC |
| ContentView (with styling) | ~60 LOC |
| CLI main.swift | 39 LOC |
| Total app code | ~224 LOC |
Infrastructure (already written):
| Layer | Lines of Code |
|---|---|
| Networking | ~150 LOC |
| Persistence | ~41 LOC |
| Use cases | ~120 LOC |
| Tests | ~500 LOC |
| Total infrastructure | ~811 LOC |
Result: 224 lines of app code leveraging 800+ lines of tested foundation.
What We Learned
- Composition Root Centralizes Complexity
Problem: Dependency creation scattered across codebase.
Solution: Single AppDependencies class.
Benefit: Change infrastructure in one place, all apps updated.
Lesson: Complexity in one place > complexity everywhere.
- Platform Differences Matter
Discovery: CLI works, macOS app doesnβt (same code).
Reason: Sandbox restrictions differ.
Solution: Understand platform security models.
Lesson: Donβt assume executables have same capabilities.
- Sandbox Violations Arenβt Always Obvious
Problem: App fails silently, no errors in Xcode console.
Discovery: Had to check Console.app (system logs).
Lesson: Know your debugging tools. Xcode != complete picture.
- Modern Swift Simplifies State Management
Old: @ObservableObject + @Published + manual change tracking.
New: @Observable + automatic observation.
Result: 30% less code, same functionality.
Lesson: Stay current with Swift evolution.
- Graceful Degradation Beats Error Messages
Choice: Show cached data instead of βNetwork Errorβ.
User impact: App feels reliable, not broken.
Lesson: Offline-first thinking improves UX.
- Clean Architecture Scales Effortlessly
Reality check: Built CLI in 20 minutes, macOS app in 1 hour (ignoring sandbox debugging).
Why so fast: Infrastructure already existed, just wired it up.
Lesson: Upfront architecture cost pays off in implementation speed.
Conclusion
We started with isolated modules: networking, persistence, use cases.
Now we have two production apps:
- CLI tool for terminal users
- macOS app with real-time UI
The journey revealed:
- Composition Root centralizes dependency creation, making everything testable and reusable
- @Observable is simpler than @ObservableObject, but requires iOS 17+
- macOS App Sandbox blocks network by default - needs explicit entitlement
- CLI tools donβt have sandbox restrictions (security trade-off)
- Graceful degradation (show cache) beats error messages for UX
The architecture paid off:
- Same AppDependencies for CLI and App
- Zero business logic duplication
- 224 LOC for both apps combined
- All backed by 500+ lines of tests
Most surprising lesson: The sandbox issue took longer to debug than building the actual apps.
Security restrictions are invisible until you hit them. Always check Console.app, not just Xcode.
Whatβs Next
The apps work, but weβre not done:
- iOS Support - Make it work on iPhone/iPad
- SwiftUI Enhancements - Charts, historical data, price alerts
- Testing the UI - ViewModel tests, snapshot tests
- CI/CD - Automated builds and releases
The foundation is solid. Networking works. Persistence works. Apps work.
Time to polish and ship π.
Appendix: Sandbox Entitlements Reference
Common entitlements for macOS apps:
| Entitlement | Permission |
|---|---|
| com.apple.security.network.client | Outgoing network connections |
| com.apple.security.network.server | Incoming network connections |
| com.apple.security.files.user-selected.read-only | Read files user chose |
| com.apple.security.files.user-selected.read-write | Read/write files user chose |
| com.apple.security.files.downloads.read-only | Read Downloads folder |
| com.apple.security.app-sandbox | Enable sandbox (required for App Store) |
Our app only needs: network.client for fetching BTC prices.
Security principle: Request minimum necessary permissions.
Resources
Essential Developer Academy
The architecture patterns, testing methodologies, and clean code principles demonstrated in this article series are inspired by the teachings of Caio Zullo and Mike Apostolakis from Essential Developer.
If you want to dive deeper into iOS architecture, TDD, Clean Architecture, and become a complete senior iOS developer, check out their iOS Lead Essentials program:
π iOS Lead Essentials Program
The program covers:
- Clean Architecture and SOLID principles
- Test-Driven Development (TDD)
- Modular design and dependency injection
- Modern Swift patterns and best practices
- Real-world project development
- Code reviews and mentoring from senior developers
Thousands of developers worldwide have transformed their careers through this program, landing positions at top companies and significantly increasing their salaries.
macOS App Sandbox Documentation
For more information about macOS App Sandbox and entitlements:
- App Sandbox Design Guide - Official Apple documentation on App Sandbox
- Entitlements Documentation - Complete list of available entitlements
- App Sandbox In Depth - Deep dive into sandbox security model
- Hardening Runtime - Additional security features for macOS apps
Related Articles in This Series
- From Requirements to Use Cases: Building a BTC Price App the Right Way - Converting requirements into clear use cases
- From Use Cases to Code: Building the Core with TDD - Domain layer and use cases with TDD
- From Core to Reality: Infrastructure, URLSession, and Real-World API Challenges - Networking layer implementation
- Persistence Decisions: UserDefaults vs FileManager vs SwiftData - Persistence layer comparison and implementation
Final Thoughts
Building this BTC price app from scratch taught us more than just how to fetch prices and display them. We learned:
- How Clean Architecture makes code testable, maintainable, and scalable
- Why TDD isnβt just about testsβitβs about design
- How Composition Root simplifies dependency management
- Why platform differences (like sandbox restrictions) matter
- That debugging system-level issues requires the right tools (Console.app)
The journey from vague requirements to production-ready apps wasnβt always smooth. We hit bugs, discovered platform quirks, and spent hours debugging sandbox issues. But each challenge reinforced the value of solid architecture and thorough testing.
If youβre serious about becoming a complete senior iOS developer and want to learn these patterns from industry experts, I highly recommend checking out the iOS Lead Essentials program by Caio Zullo and Mike Apostolakis at Essential Developer. Their methodology and teaching approach have helped thousands of developers worldwide advance their careers.
The foundation we builtβnetworking, persistence, use cases, and compositionβis now ready to scale. Whether youβre adding new features, supporting new platforms, or handling more complex requirements, the architecture will support you.
Keep building, keep learning, and remember: good architecture pays off when you need it most π
This article is part of a series on building production-ready iOS apps using Clean Architecture and TDD. The methodologies and patterns demonstrated are inspired by the teachings of Essential Developer Academy.
Related
-
- swift
- ios
- performance
Mastering Instruments (Part 4): Flame Graphs, Swift Concurrency Under the Microscope, and Processor Trace in Action
Learn to read Flame Graphs, audit async tasks with Swift Tasks, and push Processor Trace to its limits with a real CLI project that uses Swift Concurrency intensively.
-
- swift
- swift-zero-expert
- swift-fundamentals
Swift from Zero to Expert #5: Functions β first-class citizens
Parameters, labels, inout, function types and functions as values. The gateway to closures and functional programming.
-
- swift
- ios
- performance
Mastering Instruments (Part 3): Scientific Method, Advanced Time Profiler, and Profiling at Scale
Learn to diagnose performance issues as a scientific process. Master Weight vs Self-Weight, Charge/Prune/Flatten, and scale profiling with xctrace.