| name | swift-strict |
| description | Swift/SwiftUI strictness, clean code, and security rules. Use when writing, reviewing, or refactoring Swift code in iOS/macOS projects. Covers force unwrap prevention, @Observable vs ObservableObject patterns, access control, concurrency safety (@MainActor, actors, Sendable), error handling with typed enums, memory leak prevention, guard-first style, and naming conventions. Derived from production iOS apps.
|
Swift Strict Standard
Rules extracted from 3 production SwiftUI iOS apps.
CRITICAL: Unwrapping Rules
SW-01: Never force unwrap in production code
let name = user!.name
let url = URL(string: urlString)!
let day = calendar.date(byAdding: .day, value: -1, to: date)!
guard let name = user?.name else { return }
guard let url = URL(string: urlString) else {
throw AppError.invalidURL(urlString)
}
guard let day = calendar.date(byAdding: .day, value: -1, to: date) else { return }
Exceptions (very rare, must justify):
fatalError() in required init?(coder:) for programmatic-only views
@IBOutlet connections (but prefer programmatic UI)
SW-02: Use guard let for early returns, if let for scoped binding
func listNotes(in folder: String) -> [Note] {
guard let root = cloud.stikRoot else { return [] }
guard let files = try? fm.contentsOfDirectory(at: root) else { return [] }
return files.filter { ... }
}
if let fileName = removed.photoFilename {
let url = Self.photosDir.appendingPathComponent(fileName)
try? fm.removeItem(at: url)
}
Default to guard. Use if-let only when the value is needed for one branch.
SW-03: Avoid try? for critical operations
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
do {
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
} catch {
logger.error("Failed to create directory: \(error)")
throw AppError.fileSystemError(error)
}
try? fm.removeItem(at: tempFile)
CRITICAL: Error Handling
SW-04: Custom error enums with LocalizedError
enum AppError: LocalizedError {
case invalidFolderName
case containerUnavailable
case networkUnavailable
case unauthorized
case serverError(Int)
var errorDescription: String? {
switch self {
case .invalidFolderName: String(localized: "error_invalid_folder")
case .containerUnavailable: String(localized: "error_container")
case .networkUnavailable: String(localized: "error_network")
case .unauthorized: String(localized: "error_unauthorized")
case .serverError(let code): String(localized: "error_server \(code)")
}
}
}
SW-05: Use Result type for async callbacks
func handleAppleSignIn(result: Result<ASAuthorization, Error>) async {
switch result {
case .success(let auth):
await processAppleCredential(auth)
case .failure(let error):
lastError = .unknown(error)
}
}
HIGH: State Management
SW-06: Use @Observable (iOS 17+), not ObservableObject
class ViewModel: ObservableObject {
@Published var items: [Item] = []
@Published var isLoading = false
}
@Observable
final class ViewModel {
var items: [Item] = []
var isLoading = false
}
@ObservationIgnored for non-reactive internals:
@Observable final class EditorViewModel {
var content: String = ""
@ObservationIgnored
private var autosaveTask: Task<Void, Never>?
}
SW-07: @State for view-local, @Environment for injected
struct HomeView: View {
@State private var viewModel = HomeViewModel()
@Environment(\.dismiss) private var dismiss
var body: some View { ... }
}
Never use @StateObject in new code targeting iOS 17+.
SW-08: Singletons must be @Observable with private(set)
@Observable final class SavedStore {
static let shared = SavedStore()
private(set) var savedQuoteIds: [Int] = []
private(set) var folders: [SavedFolder] = []
private init() { load() }
func addToSaved(_ id: Int) {
savedQuoteIds.append(id)
persist()
}
}
HIGH: Concurrency Safety
SW-09: @MainActor on ViewModels and UI-updating services
@Observable
@MainActor
final class AuthController {
private(set) var isAuthenticated = false
private(set) var lastError: AuthError?
func signIn() async {
isLoading = true
defer { isLoading = false }
}
}
SW-10: Use actors for thread-safe services
actor APIClient {
static let shared = APIClient()
private let session = URLSession.shared
func fetch<T: Decodable>(_ endpoint: String) async throws -> T {
let (data, _) = try await session.data(from: url)
return try JSONDecoder().decode(T.self, from: data)
}
}
SW-11: No redundant MainActor.run
@MainActor func update() async {
await MainActor.run { isLoading = true }
}
@MainActor func update() async {
isLoading = true
}
SW-12: Task cancellation with [weak self]
private func scheduleAutosave() {
autosaveTask?.cancel()
autosaveTask = Task { @MainActor [weak self] in
try? await Task.sleep(for: .seconds(1.5))
guard !Task.isCancelled else { return }
self?.saveIfDirty()
}
}
Always:
- Cancel previous task before creating new one
- Check
Task.isCancelled after sleep/await
- Use
[weak self] to prevent retain cycles
HIGH: Access Control
SW-13: Default to private, widen intentionally
@Observable final class EditorViewModel {
var content: String = ""
private(set) var isDirty = false
private var autosaveTask: Task<Void, Never>?
private func scheduleAutosave() { ... }
func save() throws { ... }
}
Rules:
private for implementation details (helpers, tasks, caches)
private(set) for state that views read but shouldn't mutate
- Internal (default) for module-scoped access
public only for framework/library APIs
SW-14: final on classes by default
@Observable final class HomeViewModel { ... }
class BaseViewController: UIViewController { ... }
MEDIUM: Memory Management
SW-15: [weak self] in all async closures
NotificationCenter.default.addObserver(forName: .didChange, object: nil, queue: .main) { _ in
self.reload()
}
NotificationCenter.default.addObserver(forName: .didChange, object: nil, queue: .main) { [weak self] _ in
self?.reload()
}
SW-16: Use .task { } for view lifecycle async work
List(viewModel.items) { item in ItemRow(item: item) }
.task { await viewModel.loadItems() }
.onAppear {
Task { await viewModel.loadItems() }
}
MEDIUM: Naming & Style
SW-17: Boolean properties use is, has, can, should prefix
var isLoading = false
var isDirty = false
var isSynced = true
var hasCompletedOnboarding = false
var canEdit: Bool { ... }
SW-18: Verbs for actions, nouns for properties
func save() throws { ... }
func loadItems() async { ... }
func handleAppleSignIn(result:) async { ... }
var folders: [String] = []
var selectedFolder: String?
var errorMessage: String?
SW-19: Use .task { } modifier, not onAppear + Task
.onAppear { Task { await vm.load() } }
.task { await vm.load() }
Security & Vulnerability Rules
SW-20: Never store secrets in UserDefaults
UserDefaults.standard.set(apiKey, forKey: "api_key")
try keychain.set(apiKey, key: "api_key")
@AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding = false
SW-21: Validate URLs before loading
guard let url = URL(string: input), url.scheme == "https" else {
throw AppError.invalidURL(input)
}
SW-22: Use App Transport Security (no exceptions)
Never add NSAllowsArbitraryLoads to Info.plist unless connecting to local dev servers.
HIGH: Swift 6 Concurrency
SW-23: Sendable conformance on shared types
Swift 6 strict concurrency requires types crossing actor boundaries to be Sendable.
struct UserSession: Sendable {
let id: UUID
let token: String
}
final class Config: Sendable {
let endpoint: URL
let timeout: TimeInterval
init(endpoint: URL, timeout: TimeInterval) {
self.endpoint = endpoint
self.timeout = timeout
}
}
actor TokenStore {
private var tokens: [String: Token] = [:]
func get(_ key: String) -> Token? { tokens[key] }
}
@unchecked Sendable only with a written justification (e.g. internal lock).
SW-24: some P for return position, any P for storage
func makeView() -> some View { Text("hello") }
var providers: [any AuthProvider] = []
func makeView() -> any View { Text("hello") }
Default to some. Reach for any only when the concrete type must vary at runtime.
SW-25: Privacy manifest for App Store submission
Add PrivacyInfo.xcprivacy listing tracking domains, required reason APIs, and collected data types. Apple rejects submissions missing required-reason API declarations (UserDefaults, FileTimestamp, SystemBootTime, DiskSpace).
SW-26: Typed throws for domain errors (Swift 6+)
func parse(_ data: Data) throws -> Config { ... }
enum ConfigError: Error {
case invalidJSON
case missingField(String)
case versionMismatch(found: Int, expected: Int)
}
func parse(_ data: Data) throws(ConfigError) -> Config { ... }
do {
let cfg = try parse(data)
} catch .missingField(let f) {
}
Reach for typed throws on library boundaries and parsers. Stay untyped at top-level UI handlers where any error can bubble up.
SW-27: Compile with strict concurrency complete and warnings as errors
// Package.swift
.target(
name: "MyLib",
swiftSettings: [
.swiftLanguageMode(.v6),
.enableUpcomingFeature("StrictConcurrency"),
.unsafeFlags(["-warnings-as-errors"], .when(configuration: .release)),
]
)
Swift 6 language mode + StrictConcurrency catches data races at compile time. New code starts here.
SW-28: Use Swift 6.2 features for fixed-size and approachable concurrency
Swift 6.2 (released 2025) is the current stable. Highlights worth adopting:
InlineArray<N, T>: stack-allocated fixed-size arrays, no heap allocation, compile-time bounds. Use for small fixed buffers (RGB triples, fixed-width tokens, register sets).
- Approachable concurrency defaults: per-target setting that runs
nonisolated synchronous code on the calling actor by default. Reduces incidental awaits without giving up data-race safety.
- WebAssembly target: Swift compiles to WASM for browser and edge use cases.
- C++ interop: deeper than 6.0, viable for new bridges to existing C++ libraries.
- Containerization: official Apple project for running Linux containers natively on Apple silicon, written in Swift. Useful for build/test pipelines without Docker Desktop.
Vulnerability Checklist