Fix Recipes (iOS)
Generated 2026-05-07 · Costco iOS
Ios fix recipes — top 20
Engineering playbook of the most-applicable patterns in the codebase. Each recipe captures one decision: when to apply, the anti-pattern, the recommended pattern, and why. Use this as the team's house style.
R-i-01 · Replace try! with try? + fallback
try! crashes on any thrown error; try? lets you handle gracefully.
let regex = try! NSRegularExpression(pattern: pattern)
guard let regex = try? NSRegularExpression(pattern: pattern) else {
Logger.module.error("Regex compile failed")
return // or use fallback matcher
}
R-i-02 · Replace force-unwrap on URL(string:)
Force-unwrap on URL hides config bugs; explicit guard surfaces them.
static let endpoint = URL(string: "https://api.example.com")!
static let endpoint: URL = {
guard let u = URL(string: "https://api.example.com") else {
fatalError("compile-time URL is malformed — fix the literal")
// or in production: log + sentinel
}
return u
}()
R-i-03 · Replace as! with as? guard
as! crashes on type mismatch; as? short-circuits cleanly.
let activity = sender as! MainViewController // CCE risk
guard let activity = sender as? MainViewController else { return }
R-i-04 · Invalidate Timer in deinit
Timer retains target; without invalidate the controller leaks.
timer = Timer.scheduledTimer(
withTimeInterval: 5, repeats: true
) { _ in self.tick() }
timer = Timer.scheduledTimer(
withTimeInterval: 5, repeats: true
) { [weak self] _ in self?.tick() }
deinit {
timer?.invalidate()
timer = nil
}
R-i-05 · Capture self weakly in escaping closures
Strong self in long-lived closures creates retain cycles.
NotificationCenter.default.addObserver(
forName: .didLogin, object: nil, queue: .main
) { _ in
self.refresh()
}
// Either weak self:
NotificationCenter.default.addObserver(
forName: .didLogin, object: nil, queue: .main
) { [weak self] _ in
self?.refresh()
}
// Or migrate to Combine + cancellable:
private var cancellables = Set<AnyCancellable>()
NotificationCenter.default.publisher(for: .didLogin)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in self?.refresh() }
.store(in: &cancellables)
R-i-06 · Use weak delegate references
Without `weak`, delegate ↔ owner forms a retain cycle.
var delegate: SomeDelegate?
weak var delegate: SomeDelegate?
R-i-07 · Make asyncAfter cancellable
Default asyncAfter has no cancellation path; stale callbacks fire post-dismissal.
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.dismiss() // fires even after dismissal
}
private var dismissWork: DispatchWorkItem?
let work = DispatchWorkItem { [weak self] in
self?.dismiss()
}
dismissWork = work
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: work)
// In viewWillDisappear:
dismissWork?.cancel()
R-i-08 · Use os.Logger instead of print
Logger respects build configuration + redacts private data; print does neither.
print("User logged in: \(email)")
import os
extension Logger {
static let auth = Logger(subsystem: "com.costco.costco", category: "auth")
}
// Privacy-aware:
Logger.auth.notice("User logged in: \(email, privacy: .private)")
R-i-09 · Use Dynamic Type
Hardcoded sizes ignore Larger Text setting; users with vision impairments see clipped UI.
label.font = UIFont.systemFont(ofSize: 14)
// UIKit:
label.font = UIFont.preferredFont(forTextStyle: .body)
label.adjustsFontForContentSizeCategory = true
// SwiftUI:
Text("Hello").font(.body)
R-i-10 · Use design system tokens, not hex
Tokens enable theming, dark mode, brand consistency.
button.backgroundColor = UIColor(hex: 0xC13533)
label.foregroundColor = Color(hex: "#333333")
button.backgroundColor = PalletUIKit.brand.primary
label.foregroundColor = Pallet.text.primary
R-i-11 · Add a11y label/hint to interactive elements
VoiceOver announces nothing for icon-only buttons without labels.
Button(action: {}) { Image("cart") }
Button(action: {}) { Image("cart") }
.accessibilityLabel("Cart")
.accessibilityHint("Opens your shopping cart")
.accessibilityAddTraits(.isButton)
R-i-12 · Block screen capture on sensitive screens
Without this, the iOS app switcher shows a snapshot of payment screens.
// (no protection on payment / DMC screens)
// SwiftUI ContentView
.background(
Color.clear.onChange(of: scenePhase) { _, phase in
if phase != .active {
// Replace UI with privacy overlay or hide window content
}
}
)
// UIKit
override func applicationWillResignActive(_ application: UIApplication) {
privacyOverlay.show()
}
R-i-13 · Use sealed enums for errors
Typed errors force every caller to handle each case.
throw NSError(domain: "net", code: -1, userInfo: nil)
enum NetworkError: Error {
case http(Int, String?)
case decoding(Error)
case offline
case unknown(Error)
}
throw NetworkError.http(401, "Unauthorized")
R-i-14 · Prefer async/await over completion handlers
async/await eliminates callback pyramids and structural concurrency bugs.
func fetchUser(completion: @escaping (User?, Error?) -> Void) {
URLSession.shared.dataTask(...) { ... }.resume()
}
func fetchUser() async throws -> User {
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
R-i-15 · Adopt Privacy Manifest
Apple gates App Store submissions on Privacy Manifest since 2024.
// No PrivacyInfo.xcprivacy
<!-- PrivacyInfo.xcprivacy -->
<plist version="1.0"><dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array><string>CA92.1</string></array>
</dict>
</array>
<key>NSPrivacyTracking</key>
<false/>
</dict></plist>
R-i-16 · SwiftLint rule banning force-try
Lint catches new violations on every PR; cheaper than code review.
# .swiftlint.yml — no rule
# .swiftlint.yml
opt_in_rules:
- force_unwrapping
force_try:
severity: error
force_cast:
severity: error
force_unwrapping:
severity: error
R-i-17 · Use 44pt minimum tap target
WCAG 2.5.5 + Apple HIG mandate 44×44pt minimum.
Image("close").onTapGesture { dismiss() }
Image("close")
.frame(minWidth: 44, minHeight: 44)
.contentShape(Rectangle())
.onTapGesture { dismiss() }
.accessibilityLabel("Close")
R-i-18 · Add @Preview to public Composables/Views
Previews speed iteration; pair with snapshot tests for regression coverage.
struct CartCell: View { ... } // no preview
struct CartCell: View { ... }
#Preview("Cart cell — default") {
CartCell(item: .preview)
.padding()
}
#Preview("Cart cell — long name") {
CartCell(item: .previewLong).padding()
}
R-i-19 · Migrate Pod to SPM where available
SPM is faster, less brittle than CocoaPods; long-term direction.
# Podfile
pod 'SwiftLint'
pod 'SDWebImage'
// Package.swift in your dev tools target
dependencies: [
.package(url: "https://github.com/realm/SwiftLint", from: "0.55.0"),
.package(url: "https://github.com/SDWebImage/SDWebImage", from: "5.20.0"),
]
R-i-20 · Use String Catalogs (.xcstrings)
Single source of truth; Xcode UI editor; built-in plural support.
// Localizable.strings
"cart.title" = "Your cart";
"cart.empty" = "No items";
// Localizable.stringsdict for plurals (separate file)
// Localizable.xcstrings (Xcode 15+) — single file
{
"cart.title": { "en": { "stringUnit": { "value": "Your cart" } } },
"cart.itemCount": {
"variations": {
"plural": {
"one": { "stringUnit": { "value": "%d item" } },
"other": { "stringUnit": { "value": "%d items" } }
}
}
}
}