Swift Guide¶
Applies to: Swift 5.9+, iOS, macOS, SwiftUI, UIKit, Server-Side Swift
Core Principles¶
- Safety First: Optionals, strong typing, memory safety
- Protocol-Oriented: Prefer protocols over inheritance
- Value Types: Prefer structs over classes
- Modern Concurrency: async/await, actors, structured concurrency
- Clarity Over Brevity: Clear, expressive code
Language-Specific Guardrails¶
Swift Version & Setup¶
- ✓ Use Swift 5.9+ (latest stable)
- ✓ Use Swift Package Manager (SPM) for dependencies
- ✓ Enable strict concurrency checking
- ✓ Target minimum iOS version appropriate for features used
Code Style (Swift API Design Guidelines)¶
- ✓ Follow Swift API Design Guidelines
- ✓ Run SwiftLint before every commit
- ✓ Use
camelCasefor functions, variables, properties - ✓ Use
PascalCasefor types (structs, classes, enums, protocols) - ✓ 4-space indentation
- ✓ Line length: 120 characters
- ✓ Name methods based on their side effects (mutating vs non-mutating)
Optionals¶
- ✓ Prefer
guard letfor early exits - ✓ Use
if letfor optional binding in simple cases - ✓ Avoid force unwrapping (
!) except in tests or known-safe situations - ✓ Use nil-coalescing:
value ?? defaultValue - ✓ Use optional chaining:
object?.property?.method() - ✓ Use
mapandflatMapfor optional transformations
Value Types vs Reference Types¶
- ✓ Prefer
structoverclass(value semantics, thread-safe) - ✓ Use
classwhen identity matters or for UIKit/AppKit subclassing - ✓ Use
actorfor shared mutable state in concurrent contexts - ✓ Make properties
letby default, usevaronly when needed - ✓ Use
finalon classes that won't be subclassed
Modern Concurrency (Swift 5.5+)¶
- ✓ Use
async/awaitover completion handlers - ✓ Use
Taskfor structured concurrency - ✓ Use
actorfor thread-safe mutable state - ✓ Use
@MainActorfor UI updates - ✓ Handle
Taskcancellation properly - ✓ Use
AsyncSequencefor streams of values - ✓ Avoid
DispatchQueueunless necessary for legacy code
Error Handling¶
- ✓ Use
throwsfor functions that can fail - ✓ Use
do-catchfor error handling - ✓ Create custom
Errortypes for domain errors - ✓ Use
Resultfor async operations without async/await - ✓ Avoid
try?unless you truly don't need error details - ✓ Use
try!only when failure is impossible
Project Structure¶
iOS App Structure¶
MyApp/
├── MyApp.xcodeproj
├── MyApp/
│ ├── App/
│ │ ├── MyApp.swift # @main entry point
│ │ └── AppDelegate.swift # If using UIKit lifecycle
│ ├── Features/
│ │ ├── Authentication/
│ │ │ ├── Views/
│ │ │ ├── ViewModels/
│ │ │ └── Models/
│ │ └── Home/
│ ├── Core/
│ │ ├── Network/
│ │ ├── Storage/
│ │ └── Extensions/
│ ├── Resources/
│ │ ├── Assets.xcassets
│ │ └── Localizable.strings
│ └── Supporting Files/
│ └── Info.plist
├── MyAppTests/
├── MyAppUITests/
└── Package.swift # SPM dependencies
Swift Package Structure¶
MyPackage/
├── Package.swift
├── Sources/
│ └── MyPackage/
│ ├── MyPackage.swift
│ └── Internal/
├── Tests/
│ └── MyPackageTests/
└── README.md
SwiftUI Patterns¶
View with ViewModel¶
import SwiftUI
struct UserListView: View {
@StateObject private var viewModel = UserListViewModel()
var body: some View {
NavigationStack {
content
.navigationTitle("Users")
.task {
await viewModel.loadUsers()
}
.refreshable {
await viewModel.loadUsers()
}
}
}
@ViewBuilder
private var content: some View {
switch viewModel.state {
case .loading:
ProgressView()
case .loaded(let users):
userList(users)
case .error(let message):
errorView(message)
}
}
private func userList(_ users: [User]) -> some View {
List(users) { user in
NavigationLink(value: user) {
UserRowView(user: user)
}
}
.navigationDestination(for: User.self) { user in
UserDetailView(user: user)
}
}
private func errorView(_ message: String) -> some View {
ContentUnavailableView(
"Error",
systemImage: "exclamationmark.triangle",
description: Text(message)
)
}
}
ViewModel with @Observable (iOS 17+)¶
import Foundation
@Observable
final class UserListViewModel {
private(set) var state: ViewState = .loading
private let userService: UserServiceProtocol
init(userService: UserServiceProtocol = UserService()) {
self.userService = userService
}
@MainActor
func loadUsers() async {
state = .loading
do {
let users = try await userService.fetchUsers()
state = .loaded(users)
} catch {
state = .error(error.localizedDescription)
}
}
}
enum ViewState {
case loading
case loaded([User])
case error(String)
}
ViewModel with ObservableObject (iOS 13+)¶
import Foundation
import Combine
@MainActor
final class UserListViewModel: ObservableObject {
@Published private(set) var state: ViewState = .loading
private let userService: UserServiceProtocol
init(userService: UserServiceProtocol = UserService()) {
self.userService = userService
}
func loadUsers() async {
state = .loading
do {
let users = try await userService.fetchUsers()
state = .loaded(users)
} catch {
state = .error(error.localizedDescription)
}
}
}
Testing¶
Frameworks¶
- XCTest: Built-in testing framework
- Swift Testing (Xcode 16+): Modern testing framework
- Quick/Nimble: BDD-style testing
Guardrails¶
- ✓ Test files:
*Tests.swift - ✓ Test methods:
func test*()(XCTest) or@Test(Swift Testing) - ✓ Use descriptive names:
test_createUser_withValidData_returnsUser() - ✓ Use protocols for dependency injection (testability)
- ✓ Mock external dependencies
- ✓ Coverage target: >80% for business logic
- ✓ Test async code with expectations or async/await
Example (XCTest)¶
import XCTest
@testable import MyApp
final class UserServiceTests: XCTestCase {
private var sut: UserService!
private var mockAPIClient: MockAPIClient!
override func setUp() {
super.setUp()
mockAPIClient = MockAPIClient()
sut = UserService(apiClient: mockAPIClient)
}
override func tearDown() {
sut = nil
mockAPIClient = nil
super.tearDown()
}
func test_fetchUsers_withSuccessResponse_returnsUsers() async throws {
// Given
let expectedUsers = [User(id: "1", email: "test@example.com")]
mockAPIClient.result = .success(expectedUsers)
// When
let users = try await sut.fetchUsers()
// Then
XCTAssertEqual(users.count, 1)
XCTAssertEqual(users.first?.email, "test@example.com")
}
func test_fetchUsers_withNetworkError_throwsError() async {
// Given
mockAPIClient.result = .failure(NetworkError.connectionFailed)
// When/Then
do {
_ = try await sut.fetchUsers()
XCTFail("Expected error to be thrown")
} catch {
XCTAssertTrue(error is NetworkError)
}
}
}
// Mock
final class MockAPIClient: APIClientProtocol {
var result: Result<[User], Error> = .success([])
func fetch<T: Decodable>(_ endpoint: Endpoint) async throws -> T {
switch result {
case .success(let value):
return value as! T
case .failure(let error):
throw error
}
}
}
Example (Swift Testing - Xcode 16+)¶
import Testing
@testable import MyApp
@Suite("UserService Tests")
struct UserServiceTests {
let mockAPIClient = MockAPIClient()
let sut: UserService
init() {
sut = UserService(apiClient: mockAPIClient)
}
@Test("Fetches users successfully")
func fetchUsersSuccess() async throws {
mockAPIClient.result = .success([User(id: "1", email: "test@example.com")])
let users = try await sut.fetchUsers()
#expect(users.count == 1)
#expect(users.first?.email == "test@example.com")
}
@Test("Throws error on network failure")
func fetchUsersNetworkError() async {
mockAPIClient.result = .failure(NetworkError.connectionFailed)
await #expect(throws: NetworkError.self) {
try await sut.fetchUsers()
}
}
@Test("Validates email format", arguments: ["", "invalid", "test@"])
func invalidEmailValidation(email: String) {
#expect(throws: ValidationError.self) {
try User(email: email).validate()
}
}
}
Tooling¶
Essential Tools¶
- SwiftLint: Code style enforcement
- SwiftFormat: Code formatting
- swift-testing: Modern testing (Xcode 16+)
- xcbeautify: Readable Xcode build output
Configuration¶
# .swiftlint.yml
disabled_rules:
- trailing_whitespace
opt_in_rules:
- empty_count
- explicit_init
- fatal_error_message
- first_where
- force_unwrapping
- implicitly_unwrapped_optional
- private_action
- private_outlet
- redundant_nil_coalescing
line_length:
warning: 120
error: 150
type_body_length:
warning: 300
error: 400
file_length:
warning: 400
error: 500
function_body_length:
warning: 40
error: 60
cyclomatic_complexity:
warning: 10
error: 15
nesting:
type_level: 2
function_level: 3
custom_rules:
no_print:
name: "No print statements"
regex: "print\\("
message: "Use Logger instead of print"
severity: warning
Pre-Commit Commands¶
# Lint
swiftlint lint --strict
# Format
swiftformat .
# Build
xcodebuild build -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 15'
# Test
xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 15'
# Swift Package
swift build
swift test
Common Pitfalls¶
Don't Do This¶
// Force unwrapping without justification
let name = optionalName!
// Retain cycles in closures
class ViewModel {
var onComplete: (() -> Void)?
func setup() {
onComplete = {
self.doSomething() // Retain cycle!
}
}
}
// Blocking main thread
DispatchQueue.main.sync { } // Can deadlock
// Ignoring errors
let data = try? loadData() // Error details lost
// Massive view controllers/views
struct ContentView: View {
var body: some View {
// 500+ lines of view code
}
}
Do This Instead¶
// Safe unwrapping
guard let name = optionalName else {
return
}
// Capture list to prevent retain cycle
class ViewModel {
var onComplete: (() -> Void)?
func setup() {
onComplete = { [weak self] in
self?.doSomething()
}
}
}
// Async/await instead of blocking
@MainActor
func updateUI() async {
// UI updates here
}
// Proper error handling
do {
let data = try loadData()
} catch {
logger.error("Failed to load: \(error)")
}
// Extract subviews
struct ContentView: View {
var body: some View {
VStack {
HeaderView()
ContentListView()
FooterView()
}
}
}
Networking¶
Modern Async Networking¶
import Foundation
protocol APIClientProtocol {
func fetch<T: Decodable>(_ endpoint: Endpoint) async throws -> T
}
final class APIClient: APIClientProtocol {
private let session: URLSession
private let decoder: JSONDecoder
init(session: URLSession = .shared) {
self.session = session
self.decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
}
func fetch<T: Decodable>(_ endpoint: Endpoint) async throws -> T {
let request = try endpoint.urlRequest()
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
guard (200...299).contains(httpResponse.statusCode) else {
throw NetworkError.httpError(statusCode: httpResponse.statusCode)
}
return try decoder.decode(T.self, from: data)
}
}
enum NetworkError: Error {
case invalidURL
case invalidResponse
case httpError(statusCode: Int)
case connectionFailed
}
struct Endpoint {
let path: String
let method: HTTPMethod
let headers: [String: String]
let body: Data?
func urlRequest() throws -> URLRequest {
guard let url = URL(string: "https://api.example.com" + path) else {
throw NetworkError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
request.allHTTPHeaderFields = headers
request.httpBody = body
return request
}
}
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
Concurrency with Actors¶
Actor for Thread-Safe State¶
actor UserCache {
private var cache: [String: User] = [:]
func get(_ id: String) -> User? {
cache[id]
}
func set(_ user: User) {
cache[user.id] = user
}
func clear() {
cache.removeAll()
}
}
// Usage
let cache = UserCache()
Task {
await cache.set(user)
let cached = await cache.get("123")
}
MainActor for UI Updates¶
@MainActor
final class UserViewModel: ObservableObject {
@Published private(set) var users: [User] = []
func loadUsers() async {
// This runs on main actor, safe for @Published
let fetchedUsers = await userService.fetchUsers()
users = fetchedUsers
}
}
Performance Considerations¶
Optimization Guardrails¶
- ✓ Use
lazyfor expensive computed properties - ✓ Use value types (structs) for thread safety and copy-on-write
- ✓ Avoid unnecessary allocations in hot paths
- ✓ Use
@inlinablefor performance-critical generic functions - ✓ Profile with Instruments before optimizing
- ✓ Use
ContiguousArrayfor performance-critical array operations - ✓ Prefer
SetorDictionaryfor lookups overArray
Example¶
// Lazy initialization
struct DataProcessor {
lazy var expensiveResource: Resource = {
Resource.load()
}()
}
// Value type with copy-on-write
struct LargeData {
private var storage: Storage
private class Storage {
var data: [Int]
init(data: [Int]) { self.data = data }
}
mutating func append(_ value: Int) {
if !isKnownUniquelyReferenced(&storage) {
storage = Storage(data: storage.data)
}
storage.data.append(value)
}
}
// Efficient collection operations
let ids = Set(users.map(\.id)) // O(1) lookups
let userById = Dictionary(uniqueKeysWithValues: users.map { ($0.id, $0) })
Security Best Practices¶
Guardrails¶
- ✓ Use Keychain for sensitive data (tokens, passwords)
- ✓ Use App Transport Security (ATS) - HTTPS only
- ✓ Validate server certificates for SSL pinning (when required)
- ✓ Use
CryptoKitfor cryptographic operations - ✓ Never hardcode secrets in source code
- ✓ Use
Data Protectionfor file encryption - ✓ Validate and sanitize user input
Example¶
import Security
import CryptoKit
// Keychain storage
final class KeychainManager {
enum KeychainError: Error {
case duplicateItem
case itemNotFound
case unexpectedStatus(OSStatus)
}
func save(_ data: Data, for key: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
if status == errSecDuplicateItem {
throw KeychainError.duplicateItem
}
throw KeychainError.unexpectedStatus(status)
}
}
func retrieve(for key: String) throws -> Data {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess, let data = result as? Data else {
throw KeychainError.itemNotFound
}
return data
}
}
// Hashing with CryptoKit
func hashPassword(_ password: String) -> String {
let data = Data(password.utf8)
let hash = SHA256.hash(data: data)
return hash.compactMap { String(format: "%02x", $0) }.joined()
}