Category: 08. iOS Quality & Compliance

https://cdn3d.iconscout.com/3d/premium/thumb/compliance-3d-icon-png-download-5727631.png

  • Testing with XCTest

    Unit Tests

    Create a tests target in Xcode and add test cases that extend XCTestCase.

    Syntax: class MyTests: XCTestCase { func testSomething() { XCTAssertEqual(...) } }

    Example

    Demo.swift

    MyTests.swift

    import Foundation
    
    struct Math {
      static func add(_ a: Int, _ b: Int) -> Int { a + b }
    }

    This example defines a pure function and a unit test verifying the result with XCTAssertEqual.


    Test Target Setup

    • Add a Unit Testing Bundle target in Xcode (File → New → Target).
    • Keep test files under the tests target; import your app with @testable import when needed.
    • Use Target Membership to choose whether a file belongs to the app or tests.


    Writing Tests (AAA)

    Arrange your inputs, Act by calling the code under test, Assert on the result.

    Example

    MyTests.swift

    import XCTest
    @testable import MyApp
    
    final class MyTests: XCTestCase {
      func testAdd() {
    
    // Arrange
    let a = 2, b = 3
    // Act
    let sum = Math.add(a, b)
    // Assert
    XCTAssertEqual(sum, 5)
    } }

    XCTAssert Variants

    Assert on conditions and values.

    • XCTAssertEqualXCTAssertNotEqual
    • XCTAssertTrueXCTAssertFalse
    • XCTAssertNilXCTAssertNotNil
    • XCTAssertThrowsErrorXCTAssertNoThrow

    Async Tests

    Wait for asynchronous work to complete.

    Example

    AsyncTests.swift

    import XCTest
    
    final class AsyncTests: XCTestCase {
      func testExpectation() {
    
    let exp = expectation(description: "async work")
    DispatchQueue.global().async {
      // simulate work
      exp.fulfill()
    }
    wait(for: [exp], timeout: 1.0)
    } }

    Performance Tests

    Measure execution time and memory usage.

    Example

    PerformanceTests.swift

    import XCTest
    
    final class PerformanceTests: XCTestCase {
      func testSortPerformance() {
    
    let input = Array(0..<10_000).shuffled()
    measure {
      _ = input.sorted()
    }
    } }

    Test Plans & CI

    Organize configurations (schemes, destinations, diagnostics) and run tests in CI with xcodebuild test targeting a simulator device.

    • Use Test Plans to organize configurations (schemes, destinations, diagnostics).
    • Run tests in CI with xcodebuild test targeting a simulator device.
    • Prefer deterministic tests; mock network and time; parallelize where possible.

    UI Tests

    Use XCUIApplication to launch the app and interact with UI elements by accessibility.

    Syntax:

    • let app = XCUIApplication(); app.launch()
    • query elements with app.buttons["Label"]
    • assert with XCTAssertTrue(...)

    Example

    Demo.swift

    ContentView.swift

    App.swift

    UITests.swift

    import SwiftUI
    
    struct CounterView: View {
      @State private var count = 0
      var body: some View {
    
    VStack(spacing: 12) {
      Text("Count: \(count)")
      Button("Increment") { count += 1 }
    }
    .padding()
    } }
  • Analytics & Crash Reporting

    Xcode Organizer

    After release, use Xcode Organizer to view crash logs, energy reports, and performance metrics from users.


    Integrate Analytics SDKs

    Add third-party analytics and crash reporting SDKs via SPM or CocoaPods.

    Follow privacy best practices and disclose data collection.

    Example

    Package.swift (snippet)

    App.swift

    // Example: Add an analytics package
    // .package(url: "https://github.com/acme/analytics-sdk", from: "1.0.0")

    Add the analytics package to your project by specifying the URL and version.


    Logging (os.Logger)

    Use structured logging for diagnostics and to correlate user issues.

    Example

    Logger.swift

    import os
    
    let logger = Logger(subsystem: "com.example.app", category: "network")
    
    func fetch() async {
      logger.info("Starting fetch...")
      do {
    
    // ... network call ...
    logger.debug("Response status: \(200)")
    } catch {
    logger.error("Fetch failed: \(error.localizedDescription)")
    } }

    Track Events (with Consent)

    Design a minimal event tracker and send only necessary, non-PII data. Gate collection on user consent.

    Example

    Events.swift

    import Foundation
    
    struct Event: Codable {
      let name: String
      let props: [String: String]
    }
    
    class EventTracker {
      var consentGiven = false
      func identify(userId: String?) { /* store hashed or anonymous ID */ }
      func track(_ event: Event) {
    
    guard consentGiven else { return }
    // enqueue and batch to your endpoint
    } }

    Privacy

    • Honor user consent; provide opt-out where applicable.
    • Minimize PII; prefer anonymized, aggregated events.
    • Update your App Privacy Listing if data collection changes.

    Tip: Correlate crash reports with app versions and feature flags to speed up incident response.

    Automate symbolication for reliable stack traces.


    Crash Reporting Options

    • Xcode Organizer: Built-in crash log collection and metrics for App Store builds.
    • Third-party SDKs: Firebase Crashlytics, Sentry, etc. for real-time alerts, user/session context.
    • Symbolication & dSYMs: Ensure dSYMs are uploaded so stack traces resolve to your source lines.

    dSYMs: If using a crash SDK, configure automatic dSYM upload (e.g., Run Script Phase or CI) to avoid unsymbolicated crashes.


    Performance Metrics

    Add lightweight timing via signposts and analyze in Instruments.

    Example

    Signposts.swift

    import os
    
    let signposter = OSSignposter()
    
    func loadFeed() async {
      let id = signposter.makeSignpostID()
      let state = signposter.beginInterval("load_feed", id: id)
      // ... perform work ...
      signposter.endInterval("load_feed", state)
    }
  • In-App Purchases

    In-App Purchases (StoreKit)

    Purchase in-app products with StoreKit 2 by requesting products, purchasing, and verifying transactions.


    Product Types

    • Consumable: Can be bought multiple times (e.g., coins).
    • Non-Consumable: One-time unlock (e.g., premium upgrade).
    • Auto-Renewable Subscription: Recurring access.

    StoreKit 2 Basics

    Use StoreKit 2 to request products, purchase, and verify transactions with App Store-signed receipts.

    Syntax: let products = try await Product.products(for:)try await product.purchase(), verify and transaction.finish().

    Example

    IAP.swift

    import StoreKit
    
    @MainActor
    func buy(productID: String) async throws {
      let products = try await Product.products(for: [productID])
      guard let product = products.first else { return }
      let result = try await product.purchase()
      switch result {
      case .success(let verification):
    
    let transaction = try verification.payloadValue
    // unlock content
    await transaction.finish()
    default: break } }

    This example requests a product, performs a purchase, verifies the transaction, unlocks content, and finishes the transaction.


    Fetch Products

    Fetch products once (e.g., on app start) and keep them in memory for your paywall/store UI.

    Example

    Fetch.swift

    import StoreKit
    
    @MainActor
    func loadProducts(ids: [String]) async throws -> [Product] {
      let products = try await Product.products(for: ids)
      return products.sorted { $0.displayName < $1.displayName }
    }

    Purchase Flow & Results

    Handle all outcomes and always verify before unlocking content.

    Example

    Purchase.swift

    import StoreKit
    
    @MainActor
    func purchase(_ product: Product) async {
      do {
    
    let result = try await product.purchase()
    switch result {
    case .success(let verification):
      let transaction = try verification.payloadValue
      // unlock content guarded by transaction.productID
      await transaction.finish()
    case .userCancelled:
      // show nothing or restore UI state
      break
    case .pending:
      // family approval or SCA; update UI accordingly
      break
    @unknown default:
      break
    }
    } catch {
    // network or App Store errors
    } }

    Listen for Transaction Updates

    Receive purchases made outside your app UI (e.g., from the App Store or another device) and finish them.

    Example

    Updates.swift

    import StoreKit
    
    func startTransactionListener() {
      Task.detached {
    
    for await update in Transaction.updates {
      do {
        let transaction = try update.payloadValue
        // update entitlements based on transaction.productID
        await transaction.finish()
      } catch {
        // handle verification failure
      }
    }
    } }

    Restore Purchases

    Query current entitlements to restore non-consumables and active subscriptions.

    Example

    Restore.swift

    import StoreKit
    
    @MainActor
    func restore() async {
      for await result in Transaction.currentEntitlements {
    
    if let transaction = try? result.payloadValue {
      // re-unlock features for transaction.productID
    }
    } }
  • App Privacy

    Privacy Nutrition Labels

    In App Store Connect, you must disclose what data your app collects and how it is used (tracking, third-party sharing, linking to the user, etc.).


    Data Types

    • Contact info, health/fitness, financial info
    • Location, sensitive info, contacts, user content
    • Identifiers, usage data, diagnostics

    Best Practices

    • Collect only what you need; prefer on-device processing.
    • Provide clear in-app explanations and opt-in flows.
    • Offer deletion and export of user data where applicable.

    Tip: If you use third-party SDKs (analytics, ads, crash reporting), include their data practices in your disclosure.


    What Apple Asks

    • Is data collected? Collected means transmitted off device to you or a third party.
    • Is data linked to the user? “Linked” means it’s associated with identity (account, device ID, etc.).
    • Is data used for tracking? Tracking means linking data across apps/websites owned by other companies for ads/measurement. Requires App Tracking Transparency.

    How to Fill It In (App Store Connect)

    • Per data type (e.g., Identifiers, Usage Data, Diagnostics), answer:
      • Collected? If yes, by you or third parties?
      • Linked to the user? If not, aggregate or de-identified.
      • Purpose: App Functionality, Analytics, Developer’s Advertising, Third-Party Advertising, Product Personalization, etc.
      • Tracking? Check only if used for cross-app/company tracking.
    • Minimize scope: If you only collect during opt-in flows, reflect that and describe controls in your privacy policy.

    Example

    Example: Analytics (first-party)

    Data Type: Usage Data
    Collected: Yes (by you)
    Linked to User: Yes
    Purpose: Analytics
    Tracking: No

    Common Examples

    • Analytics (first-party): Usage Data (collected), often linked; purpose: Analytics; not used for tracking.
    • Crash reporting: Diagnostics (collected); usually linked for debugging; purpose: App Functionality/Diagnostics; not used for tracking.
    • Ads SDKs: Identifiers (IDFA), Usage Data; collected and linked; purpose: Third-Party Advertising; tracking = yes (requires ATT prompt).
    • Push notifications: Device token used to deliver pushes; disclose if you associate tokens with user identity or analytics.
    • Location features: Location data; specify purpose (App Functionality like maps vs. Ads/Analytics) and whether linked/tracked.

    Third-party SDKs: Review each SDK’s data collection. Your disclosure must include SDK behaviors (analytics, ads, crash, social sign-in, etc.).


    Updates & Maintenance

    • Revisit disclosures when you add features/SDKs or change analytics/ads settings.
    • Keep your privacy policy URL live and consistent with the listing.
    • Document data retention and deletion practices; provide user-initiated deletion if applicable.
  • Accessibility

    VoiceOver and Labels

    All interactive elements must be perceivable to assistive technologies.

    Provide meaningful labels and hints.

    Example

    SwiftUI

    import SwiftUI
    
    struct ContentView: View {
      @State private var count = 0
      var body: some View {
    
    VStack(spacing: 12) {
      Text("Count: \(count)")
        .accessibilityLabel("Current count")
        .accessibilityValue("\(count)")
      Button("Increment") { count += 1 }
        .accessibilityHint("Increases the count by one")
    }
    .padding()
    } }

    In this example, the count is perceivable through both label and value.


    Dynamic Type and Contrast

    Use system text styles (e.g., .font(.body)) and check color contrast.

    Respect reduced motion and increased contrast settings.

    Tip: Test with VoiceOver, Larger Text, Bold Text, and different Appearance (Light/Dark).

    Use the Accessibility Inspector in Xcode.


    Semantics & Traits

    Expose clear semantics and traits so assistive tech understands your UI.

    Example

    Semantics.swift

    import SwiftUI
    
    struct PlanBadge: View {
      var body: some View {
    
    VStack {
      Image(systemName: "star.fill")
        .foregroundStyle(.yellow)
        .accessibilityLabel("Favorite")
        .accessibilityAddTraits(.isImage)
      Text("Premium plan")
    }
    // Read as one element: "Favorite, Premium plan"
    .accessibilityElement(children: .combine)
    } }

    In the example above, the image is exposed as an image and the text as a label.


    Focus, Order & Grouping

    Control reading order and group related elements.

    Example

    Order.swift

    import SwiftUI
    
    struct TotalRow: View {
      var total: Double
      var body: some View {
    
    HStack {
      Text("Total")
        .accessibilitySortPriority(2)
      Text(total, format: .currency(code: Locale.current.currency?.identifier ?? "USD"))
        .accessibilitySortPriority(1)
    }
    // Alternatively, combine for a single announcement
    // .accessibilityElement(children: .combine)
    } }

    In the example above, the total is read as one element.


    Custom Actions

    Expose adjustable and named actions to VoiceOver users.

    Example

    Actions.swift

    import SwiftUI
    
    struct QuantityStepper: View {
      @State private var qty = 1
      var body: some View {
    
    Stepper("Quantity: \(qty)", value: $qty)
      .accessibilityValue("\(qty)")
      .accessibilityAdjustableAction { direction in
        switch direction {
        case .increment: qty += 1
        case .decrement: qty = max(0, qty - 1)
        default: break
        }
      }
      .accessibilityAction(named: Text("Reset")) { qty = 1 }
    } }
  • Localization

    Localize Strings

    Add Localizable.strings per language and use NSLocalizedString or SwiftUI’s Text("key").

    Syntax: "key" = "value"; in each Localizable.strings, read with NSLocalizedString("key", comment: "") or Text("key").

    Example

    Localizable.strings (en)

    Localizable.strings (es)

    SwiftUI

    "hello" = "Hello";
    "welcome_name" = "Welcome, %@";

    Pluralization (.stringsdict)

    Use .stringsdict to support plural rules that vary by language.

    Example

    Localizable.stringsdict (en)

    SwiftUI usage

    {
      "apples_count" : {
    
    "NSStringLocalizedFormatKey" : "%#@apples@",
    "apples" : {
      "NSStringFormatSpecTypeKey" : "NSStringPluralRuleType",
      "NSStringFormatValueTypeKey" : "d",
      "one" : "%d apple",
      "other" : "%d apples"
    }
    } }

    SwiftUI Localization Tips

    • Prefer Text("key") for static keys present in Localizable.strings.
    • For values, compose with String.localizedStringWithFormat and a localized key (supports .stringsdict).
    • Use system formatters for locale-aware output: numbers, dates, currency.

    Example

    Formatters.swift

    import SwiftUI
    
    struct PricesView: View {
      let price: Double = 12.5
      let date = Date()
      var body: some View {
    
    VStack(alignment: .leading) {
      Text(price, format: .currency(code: Locale.current.currency?.identifier ?? "USD"))
      Text(date, style: .date)
    }
    } }

    Testing Localization

    • In Xcode scheme, set Application Language to a target language or a Pseudolanguage to catch truncation.
    • Test Right-to-Left using the Right-to-Left Pseudolanguage and verify UI flips correctly.
    • Avoid hard-coded strings; every user-facing string should be a key in Localizable.strings.
    • Use String Catalogs in modern Xcode to manage keys and translations at scale.