Skip to main content

iOS Development Overview

iOS Development Essentials

Use Keychain (not UserDefaults) for credential storage with kSecAttrAccessibleWhenUnlockedThisDeviceOnly. Implement biometric authentication via LocalAuthentication framework. Build offline-first with Core Data for local persistence and background URLSession for sync. SwiftUI provides type-safe, declarative UI. Combine enables reactive data flows. Target: 4.5+ App Store rating, >99.5% crash-free sessions, <2s cold app launch.

Overview

This guide provides a comprehensive overview of iOS development best practices. We cover project setup, recommended versions, project structure following Clean Architecture principles (see Architecture Overview for foundational concepts), development workflow, dependency injection, and core architectural principles that form the foundation of professional iOS development.

What This Guide Covers

  • Project Setup: Xcode configuration, Swift versions, deployment targets
  • Project Structure: Clean Architecture folder organization for iOS
  • Development Workflow: From development to App Store submission
  • Dependency Injection: Managing dependencies for testability and maintainability
  • Code Review Checklist: Common mistakes and best practices for iOS code reviews

This overview sets the foundation. For specialized topics, see:


Core Principles

1. Swift First

Modern Swift patterns, value types, protocol-oriented programming. Avoid Objective-C legacy patterns.

2. SwiftUI Declarative UI

State-driven UI with minimal imperative code. SwiftUI is the standard for new development.

3. MVVM Architecture

Clear separation between views and business logic. Views observe ViewModels via @Published properties.

4. Combine for Reactivity

Publisher/Subscriber pattern for data streams. Use async/await for simpler asynchronous code.

5. Offline-First

Core Data with local caching and sync strategies. Applications must work offline for viewing balances and transactions.

6. Keychain Security

Never use UserDefaults for sensitive data. Always use Keychain for tokens, PINs, credentials.

7. Type Safety

Leverage Swift's strong type system to prevent runtime errors. Use enums, optionals, and Result types.

8. Testability

Design for testability from the start. Use dependency injection, protocol-based abstractions, and pure functions.


iOS Development Lifecycle


Project Setup

// Minimum deployment target
iOS 16.0+ // Latest - 2 versions for applications

// Swift version
Swift 5.9+

// Xcode version
Xcode 15.0+
Version Compatibility

Applications must support iOS versions from the last 2-3 years to reach 95%+ of customers. Monitor iOS version adoption at Apple's Platform State of the Union.

Xcode Project Configuration

<!-- Info.plist essential settings -->
<key>CFBundleDisplayName</key>
<string>BankApp</string>

<key>NSFaceIDUsageDescription</key>
<string>Authenticate securely to access your account</string>

<key>NSCameraUsageDescription</key>
<string>Scan checks and documents for deposit</string>

<key>NSLocationWhenInUseUsageDescription</key>
<string>Find nearby ATMs and branches</string>

<!-- App Transport Security -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<!-- Certificate pinning enforced -->
</dict>

<!-- Prevent screenshots in sensitive screens -->
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>

Build Configuration

// Build Settings for Banking Apps

// Debug Configuration
DEBUG = YES
SWIFT_OPTIMIZATION_LEVEL = -Onone
ENABLE_TESTABILITY = YES

// Release Configuration
DEBUG = NO
SWIFT_OPTIMIZATION_LEVEL = -O
ENABLE_TESTABILITY = NO
VALIDATE_PRODUCT = YES
STRIP_SWIFT_SYMBOLS = YES

// Security Settings (Both)
ENABLE_BITCODE = NO // Deprecated in Xcode 14
CODE_SIGN_STYLE = Manual
CODE_SIGN_IDENTITY = "Apple Distribution"
PROVISIONING_PROFILE_SPECIFIER = "BankApp Production Profile"

Project Structure

The iOS project structure applies Clean Architecture (see Architecture Overview) with clear separation between presentation (SwiftUI/ViewModels), domain (pure Swift business logic), and data (URLSession/Core Data) layers. The domain layer has zero dependencies on iOS frameworks, making business logic fully testable without views or network clients.

Clean Architecture Directory Organization

BankingApp/
├── BankingApp/
│ ├── App/
│ │ ├── BankingApp.swift # App entry point (@main)
│ │ └── AppDelegate.swift # Optional for legacy support
│ │
│ ├── Core/ # Core utilities and shared code
│ │ ├── Network/
│ │ │ ├── NetworkManager.swift # URLSession wrapper
│ │ │ ├── APIEndpoint.swift # API endpoint definitions
│ │ │ └── NetworkError.swift # Network error types
│ │ ├── Storage/
│ │ │ ├── KeychainManager.swift # Keychain wrapper
│ │ │ └── CoreDataStack.swift # Core Data setup
│ │ ├── Security/
│ │ │ ├── BiometricAuth.swift # Face ID/Touch ID
│ │ │ └── CertificatePinning.swift # SSL pinning
│ │ └── Extensions/
│ │ ├── String+Extensions.swift
│ │ ├── Date+Extensions.swift
│ │ └── View+Extensions.swift
│ │
│ ├── Domain/ # Business logic layer (framework-independent)
│ │ ├── Models/
│ │ │ ├── Payment.swift
│ │ │ ├── Account.swift
│ │ │ └── Customer.swift
│ │ ├── Repositories/
│ │ │ ├── PaymentRepository.swift # Protocol
│ │ │ └── AccountRepository.swift # Protocol
│ │ └── UseCases/
│ │ ├── GetPaymentsUseCase.swift
│ │ └── CreatePaymentUseCase.swift
│ │
│ ├── Data/ # Data layer (implements repositories)
│ │ ├── Remote/
│ │ │ ├── DTOs/
│ │ │ │ ├── PaymentDTO.swift
│ │ │ │ └── AccountDTO.swift
│ │ │ └── Services/
│ │ │ ├── PaymentService.swift
│ │ │ └── AccountService.swift
│ │ ├── Local/
│ │ │ ├── CoreData/
│ │ │ │ ├── BankingApp.xcdatamodeld
│ │ │ │ ├── PaymentEntity+CoreDataClass.swift
│ │ │ │ └── PaymentEntity+CoreDataProperties.swift
│ │ │ └── DAO/
│ │ │ ├── PaymentDAO.swift
│ │ │ └── AccountDAO.swift
│ │ └── Repositories/
│ │ └── PaymentRepositoryImpl.swift # Implements protocol
│ │
│ ├── Presentation/ # UI layer
│ │ ├── Screens/
│ │ │ ├── Payments/
│ │ │ │ ├── PaymentListView.swift
│ │ │ │ ├── PaymentListViewModel.swift
│ │ │ │ ├── PaymentDetailView.swift
│ │ │ │ └── PaymentDetailViewModel.swift
│ │ │ ├── Auth/
│ │ │ │ ├── LoginView.swift
│ │ │ │ └── LoginViewModel.swift
│ │ │ └── Profile/
│ │ │ ├── ProfileView.swift
│ │ │ └── ProfileViewModel.swift
│ │ ├── Components/
│ │ │ ├── PaymentCard.swift
│ │ │ ├── LoadingView.swift
│ │ │ └── ErrorView.swift
│ │ ├── Navigation/
│ │ │ ├── AppCoordinator.swift
│ │ │ └── NavigationPath.swift
│ │ └── Theme/
│ │ ├── Colors.swift
│ │ ├── Typography.swift
│ │ └── Spacing.swift
│ │
│ ├── Resources/
│ │ ├── Assets.xcassets
│ │ ├── Localizable.strings
│ │ └── Info.plist
│ │
│ └── DI/ # Dependency Injection
│ └── DIContainer.swift

├── BankingAppTests/ # Unit tests
├── BankingAppUITests/ # UI tests
└── Podfile / Package.swift # Dependencies

Architecture Layers Diagram

Dependency Flow Diagram

Dependency Rule

Domain layer has ZERO dependencies on other layers. The domain layer contains pure business logic without any framework or infrastructure dependencies. This isolation enables testing domain logic independently of UI frameworks, databases, or network clients. Data and Presentation layers depend on Domain through protocols (Dependency Inversion Principle).


Dependency Injection

DIContainer Pattern

// DI/DIContainer.swift
import Foundation

final class DIContainer {
static let shared = DIContainer()

private init() {}

// MARK: - Core Services

lazy var networkManager: NetworkManagerProtocol = {
NetworkManager(session: .shared)
}()

lazy var coreDataStack: CoreDataStack = {
CoreDataStack.shared
}()

lazy var keychainManager: KeychainManager = {
KeychainManager.shared
}()

// MARK: - Data Layer

lazy var paymentDAO: PaymentDAOProtocol = {
PaymentDAO(coreDataStack: coreDataStack)
}()

// MARK: - Repositories

lazy var paymentRepository: PaymentRepository = {
PaymentRepositoryImpl(
networkManager: networkManager,
paymentDAO: paymentDAO
)
}()

// MARK: - Use Cases

lazy var getPaymentsUseCase: GetPaymentsUseCase = {
GetPaymentsUseCaseImpl(repository: paymentRepository)
}()

lazy var createPaymentUseCase: CreatePaymentUseCase = {
CreatePaymentUseCaseImpl(repository: paymentRepository)
}()
}

Environment Object for Dependency Injection

// App/BankingApp.swift
import SwiftUI

@main
struct BankingApp: App {
let container = DIContainer.shared

var body: some Scene {
WindowGroup {
AppCoordinatorView()
.environmentObject(container)
}
}
}

// Usage in View
struct PaymentListView: View {
@EnvironmentObject var container: DIContainer
@StateObject private var viewModel: PaymentListViewModel

init() {
// Inject dependencies
let container = DIContainer.shared
_viewModel = StateObject(wrappedValue: PaymentListViewModel(
getPaymentsUseCase: container.getPaymentsUseCase
))
}

var body: some View {
// View implementation
}
}

Dependency Injection Diagram


Development Workflow

Feature Development Flow

Build Configuration Workflow


Environment Configuration

Configuration Files

// Core/Config/Environment.swift
import Foundation

enum Environment {
case development
case staging
case production

static var current: Environment {
#if DEBUG
return .development
#elseif STAGING
return .staging
#else
return .production
#endif
}

var baseURL: String {
switch self {
case .development:
return "https://api-dev.bank.com"
case .staging:
return "https://api-staging.bank.com"
case .production:
return "https://api.bank.com"
}
}

var isProduction: Bool {
self == .production
}

var enableLogging: Bool {
self != .production
}
}

Build Schemes

Development Scheme:
- DEBUG preprocessor flag
- Development certificate
- Development provisioning profile
- Detailed logging enabled

Staging Scheme:
- STAGING preprocessor flag
- Ad Hoc certificate
- Ad Hoc provisioning profile
- Reduced logging

Production Scheme:
- No debug flags
- Distribution certificate
- App Store provisioning profile
- Minimal logging (errors only)

Common Mistakes

Don't: Use Force Unwrapping

//  BAD: Force unwrapping can crash the app
let payment = payments.first!
let amount = Decimal(string: amountString)!

// GOOD: Use optional binding or nil coalescing
guard let payment = payments.first else {
print("No payments found")
return
}

guard let amount = Decimal(string: amountString) else {
throw ValidationError.invalidAmount
}

Don't: Hardcode Strings

//  BAD: Hardcoded strings
label.text = "Payment Successful"

// GOOD: Use localization
label.text = NSLocalizedString("payment.success", comment: "Payment success message")

// Or SwiftUI
Text("payment.success")

Don't: Ignore Memory Management

//  BAD: Retain cycle in closure
class PaymentViewModel {
func loadPayments() {
networkManager.fetch { response in
self.payments = response.payments // RETAIN CYCLE!
}
}
}

// GOOD: Use [weak self]
class PaymentViewModel {
func loadPayments() {
networkManager.fetch { [weak self] response in
self?.payments = response.payments
}
}
}

Don't: Block Main Thread

//  BAD: Synchronous operation on main thread
func loadImage(from url: URL) -> UIImage? {
let data = try? Data(contentsOf: url) // BLOCKS UI!
return data.flatMap { UIImage(data: $0) }
}

// GOOD: Use async/await
func loadImage(from url: URL) async throws -> UIImage {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageError.invalidData
}
return image
}

Don't: Use UserDefaults for Sensitive Data

//  BAD: Storing auth token in UserDefaults (insecure!)
UserDefaults.standard.set(token, forKey: "auth_token")

// GOOD: Use Keychain
KeychainManager.shared.saveAuthToken(token)

Code Review Checklist

Security Red Flags (Block PR)

  • No sensitive data in UserDefaults - Use Keychain for tokens, PINs, credentials
  • No hardcoded secrets - API keys, tokens, passwords must come from secure config
  • Certificate pinning implemented - For production API calls
  • No sensitive data logged - Account numbers, PINs, tokens must not be logged
  • Biometric auth has fallback - Must support PIN entry if biometrics fail
  • No force unwrapping optional secrets - Keychain reads can return nil

Watch For (Request Changes)

  • Avoid force unwrapping (!) - Use guard/if let or nil coalescing
  • Check for retain cycles - Use [weak self] in closures
  • Main thread blocking - Network/file operations must be async
  • Memory leaks - ViewModels, closures, delegates properly deallocated
  • Large view files (500+ lines) - Break into smaller components
  • Implicitly unwrapped optionals (!) - Only use for IBOutlets
  • Missing error handling - All async operations must handle errors
  • Deprecated APIs - Check for iOS deprecation warnings

Best Practices (Approve if Present)

  • Dependency injection used - ViewModels receive dependencies via init
  • Protocol-based abstractions - NetworkManager, repositories use protocols
  • Unit tests included - New business logic has test coverage
  • Localized strings - No hardcoded user-facing text
  • SwiftUI previews - All views have #Preview
  • Proper async/await usage - Not blocking main thread
  • Guard statements - Early returns for invalid state
  • Meaningful variable names - No single-letter names except loop indices

Critical Checks

  • Decimal for currency - Never use Float/Double for money
  • Audit logging - Payment actions logged with user ID, timestamp
  • Offline support - Core features work without network
  • Transaction idempotency - Payment creation uses idempotency keys
  • Input validation - Account numbers, amounts validated before API calls
  • Accessibility - VoiceOver labels for critical actions

Performance Considerations

App Launch Time

Target: Cold launch < 2 seconds, warm launch < 0.5 seconds

Memory Management

// Use weak references to avoid retain cycles
class PaymentViewModel: ObservableObject {
private weak var coordinator: AppCoordinator?

init(coordinator: AppCoordinator) {
self.coordinator = coordinator
}
}

// Lazy initialization for expensive objects
lazy var imageCache: NSCache<NSString, UIImage> = {
let cache = NSCache<NSString, UIImage>()
cache.countLimit = 100
cache.totalCostLimit = 50 * 1024 * 1024 // 50 MB
return cache
}()

Further Reading

iOS Framework Guidelines

Language and Cross-Platform Guidelines

External Resources


Summary

Key Takeaways

  1. Swift-first development - Modern Swift patterns, avoid Objective-C legacy
  2. Clean Architecture - Separation of concerns with clear dependency flow
  3. Dependency Injection - DIContainer for testability and flexibility
  4. Type Safety - Leverage Swift optionals, enums, and Result types
  5. Security First - Keychain for secrets, biometric authentication, certificate pinning
  6. Offline-First - Core Data for local storage, sync strategies data
  7. Environment Configuration - Dev/Staging/Production configurations
  8. Code Review Standards - Security, memory management, thread safety
  9. Performance Targets - <2s cold launch, >99.5% crash-free rate
  10. App Store Compliance - Follow Apple guidelines for applications

Next Steps: