iOS Development Overview
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
Related iOS Guides
This overview sets the foundation. For specialized topics, see:
- iOS UI Development - SwiftUI, navigation, design systems
- iOS Data & Networking - URLSession, Core Data, repositories
- iOS Security - Keychain, biometrics, certificate pinning
- iOS Architecture - MVVM, Clean Architecture, patterns
- iOS Testing - XCTest, UI testing, TDD
- iOS Performance - Instruments, optimization, profiling
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
Recommended Versions
// Minimum deployment target
iOS 16.0+ // Latest - 2 versions for applications
// Swift version
Swift 5.9+
// Xcode version
Xcode 15.0+
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
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
- iOS UI Development - SwiftUI, navigation, and design systems
- iOS Data & Networking - URLSession, Core Data, repositories
- iOS Security - Keychain, biometrics, certificate pinning
- iOS Architecture - MVVM, Clean Architecture, Coordinator pattern
- iOS Testing - XCTest, UI testing, and TDD strategies
- iOS Performance - Instruments, profiling, and optimization
Language and Cross-Platform Guidelines
- Swift Best Practices - Swift language guidelines and idioms
- Swift Concurrency - Async/await and actors
- Mobile Overview - Cross-platform mobile guidance
- Mobile Navigation - Navigation patterns
- Architecture Overview - Clean Architecture and design principles
- Security Overview - Cross-platform security principles
- API Overview - RESTful API principles
- OpenAPI Frontend Integration - API client generation
External Resources
- Apple Human Interface Guidelines
- Swift Documentation
- App Store Review Guidelines
- WWDC Videos
- Swift by Sundell
Summary
Key Takeaways
- Swift-first development - Modern Swift patterns, avoid Objective-C legacy
- Clean Architecture - Separation of concerns with clear dependency flow
- Dependency Injection - DIContainer for testability and flexibility
- Type Safety - Leverage Swift optionals, enums, and Result types
- Security First - Keychain for secrets, biometric authentication, certificate pinning
- Offline-First - Core Data for local storage, sync strategies data
- Environment Configuration - Dev/Staging/Production configurations
- Code Review Standards - Security, memory management, thread safety
- Performance Targets - <2s cold launch, >99.5% crash-free rate
- App Store Compliance - Follow Apple guidelines for applications
Next Steps:
- Explore iOS UI Development for SwiftUI patterns and navigation
- Review iOS Security -specific security requirements
- Study iOS Architecture for advanced architectural patterns