Overview › Fix Recipes (iOS)

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.

RECIPE

R-i-01 · Replace try! with try? + fallback

try! crashes on any thrown error; try? lets you handle gracefully.

Before — anti-pattern
let regex = try! NSRegularExpression(pattern: pattern)
After — recommended
guard let regex = try? NSRegularExpression(pattern: pattern) else {
    Logger.module.error("Regex compile failed")
    return  // or use fallback matcher
}
RECIPE

R-i-02 · Replace force-unwrap on URL(string:)

Force-unwrap on URL hides config bugs; explicit guard surfaces them.

Before — anti-pattern
static let endpoint = URL(string: "https://api.example.com")!
After — recommended
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
}()
RECIPE

R-i-03 · Replace as! with as? guard

as! crashes on type mismatch; as? short-circuits cleanly.

Before — anti-pattern
let activity = sender as! MainViewController  // CCE risk
After — recommended
guard let activity = sender as? MainViewController else { return }
RECIPE

R-i-04 · Invalidate Timer in deinit

Timer retains target; without invalidate the controller leaks.

Before — anti-pattern
timer = Timer.scheduledTimer(
    withTimeInterval: 5, repeats: true
) { _ in self.tick() }
After — recommended
timer = Timer.scheduledTimer(
    withTimeInterval: 5, repeats: true
) { [weak self] _ in self?.tick() }

deinit {
    timer?.invalidate()
    timer = nil
}
RECIPE

R-i-05 · Capture self weakly in escaping closures

Strong self in long-lived closures creates retain cycles.

Before — anti-pattern
NotificationCenter.default.addObserver(
    forName: .didLogin, object: nil, queue: .main
) { _ in
    self.refresh()
}
After — recommended
// 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)
RECIPE

R-i-06 · Use weak delegate references

Without `weak`, delegate ↔ owner forms a retain cycle.

Before — anti-pattern
var delegate: SomeDelegate?
After — recommended
weak var delegate: SomeDelegate?
RECIPE

R-i-07 · Make asyncAfter cancellable

Default asyncAfter has no cancellation path; stale callbacks fire post-dismissal.

Before — anti-pattern
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
    self.dismiss()  // fires even after dismissal
}
After — recommended
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()
RECIPE

R-i-08 · Use os.Logger instead of print

Logger respects build configuration + redacts private data; print does neither.

Before — anti-pattern
print("User logged in: \(email)")
After — recommended
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)")
RECIPE

R-i-09 · Use Dynamic Type

Hardcoded sizes ignore Larger Text setting; users with vision impairments see clipped UI.

Before — anti-pattern
label.font = UIFont.systemFont(ofSize: 14)
After — recommended
// UIKit:
label.font = UIFont.preferredFont(forTextStyle: .body)
label.adjustsFontForContentSizeCategory = true

// SwiftUI:
Text("Hello").font(.body)
RECIPE

R-i-10 · Use design system tokens, not hex

Tokens enable theming, dark mode, brand consistency.

Before — anti-pattern
button.backgroundColor = UIColor(hex: 0xC13533)
label.foregroundColor = Color(hex: "#333333")
After — recommended
button.backgroundColor = PalletUIKit.brand.primary
label.foregroundColor = Pallet.text.primary
RECIPE

R-i-11 · Add a11y label/hint to interactive elements

VoiceOver announces nothing for icon-only buttons without labels.

Before — anti-pattern
Button(action: {}) { Image("cart") }
After — recommended
Button(action: {}) { Image("cart") }
    .accessibilityLabel("Cart")
    .accessibilityHint("Opens your shopping cart")
    .accessibilityAddTraits(.isButton)
RECIPE

R-i-12 · Block screen capture on sensitive screens

Without this, the iOS app switcher shows a snapshot of payment screens.

Before — anti-pattern
// (no protection on payment / DMC screens)
After — recommended
// 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()
}
RECIPE

R-i-13 · Use sealed enums for errors

Typed errors force every caller to handle each case.

Before — anti-pattern
throw NSError(domain: "net", code: -1, userInfo: nil)
After — recommended
enum NetworkError: Error {
    case http(Int, String?)
    case decoding(Error)
    case offline
    case unknown(Error)
}

throw NetworkError.http(401, "Unauthorized")
RECIPE

R-i-14 · Prefer async/await over completion handlers

async/await eliminates callback pyramids and structural concurrency bugs.

Before — anti-pattern
func fetchUser(completion: @escaping (User?, Error?) -> Void) {
    URLSession.shared.dataTask(...) { ... }.resume()
}
After — recommended
func fetchUser() async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}
RECIPE

R-i-15 · Adopt Privacy Manifest

Apple gates App Store submissions on Privacy Manifest since 2024.

Before — anti-pattern
// No PrivacyInfo.xcprivacy
After — recommended
<!-- 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>
RECIPE

R-i-16 · SwiftLint rule banning force-try

Lint catches new violations on every PR; cheaper than code review.

Before — anti-pattern
# .swiftlint.yml — no rule
After — recommended
# .swiftlint.yml
opt_in_rules:
  - force_unwrapping
force_try:
  severity: error
force_cast:
  severity: error
force_unwrapping:
  severity: error
RECIPE

R-i-17 · Use 44pt minimum tap target

WCAG 2.5.5 + Apple HIG mandate 44×44pt minimum.

Before — anti-pattern
Image("close").onTapGesture { dismiss() }
After — recommended
Image("close")
    .frame(minWidth: 44, minHeight: 44)
    .contentShape(Rectangle())
    .onTapGesture { dismiss() }
    .accessibilityLabel("Close")
RECIPE

R-i-18 · Add @Preview to public Composables/Views

Previews speed iteration; pair with snapshot tests for regression coverage.

Before — anti-pattern
struct CartCell: View { ... }  // no preview
After — recommended
struct CartCell: View { ... }

#Preview("Cart cell — default") {
    CartCell(item: .preview)
        .padding()
}
#Preview("Cart cell — long name") {
    CartCell(item: .previewLong).padding()
}
RECIPE

R-i-19 · Migrate Pod to SPM where available

SPM is faster, less brittle than CocoaPods; long-term direction.

Before — anti-pattern
# Podfile
pod 'SwiftLint'
pod 'SDWebImage'
After — recommended
// 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"),
]
RECIPE

R-i-20 · Use String Catalogs (.xcstrings)

Single source of truth; Xcode UI editor; built-in plural support.

Before — anti-pattern
// Localizable.strings
"cart.title" = "Your cart";
"cart.empty" = "No items";

// Localizable.stringsdict for plurals (separate file)
After — recommended
// 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" } }
      }
    }
  }
}
Costco iOS · Code Review Report · Generated 2026-05-07 · 88 machine-curated findings