Home > Developer Guide > Guides > Kotlin-Swift Type Interoperability


Kotlin-Swift Type Interoperability Patterns

Purpose: Prevent hours of whack-a-mole debugging when working with Kotlin types in Swift

Context: Discovered during Week 28 iOS Firebase integration (PR #266)

Impact: Required vs optional parameter conversion behaves differently - understanding this pattern is essential for iOS KMP development

Overview

Why This Matters

When integrating Kotlin Multiplatform (KMP) code with Swift, type conversion between platforms follows specific rules that are not immediately obvious. During the Week 28 iOS Firebase integration, we spent approximately 2 hours debugging type conversion errors that appeared one-by-one in Xcode.

The Problem:

  • Xcode compiler stops at the first type error
  • Fixing one error reveals the next error (whack-a-mole pattern)
  • Each compilation cycle takes time
  • Without understanding the full pattern, you’ll encounter errors repeatedly

The Solution: This guide documents all type conversion patterns discovered during real Firebase integration, allowing future developers to write correct code the first time.

When You’ll Need This

You’ll encounter these patterns when:

  • Creating Kotlin objects from Swift code (e.g., parsing Firebase documents)
  • Calling Kotlin constructors with Swift values
  • Converting between Firestore NSNumber types and Kotlin numeric types
  • Working with Kotlin enums from Swift
  • Handling optional vs required parameters in Kotlin data classes

Type Conversion Rules

The Core Principle

Required vs Optional parameters have different conversion behaviors:

Kotlin TypeSwift Required ParameterSwift Optional Parameter
Int (Int32)Int32 (auto-converts)KotlinInt? (must wrap)
Long (Int64)Int64 (auto-converts)KotlinLong? (must wrap)
StringString (auto-converts)String? (auto-converts)
BooleanBool (auto-converts)Bool? (auto-converts)
List<T>[T] (auto-converts)[T]? (auto-converts)
EnumStatic properties (.open)Static properties (.open)

Key Insight:

  • Numeric types (Int32, Int64): Auto-convert for required parameters, but must be wrapped in KotlinInt? / KotlinLong? for optional parameters
  • Strings, Booleans, Arrays: Auto-convert for both required and optional
  • Enums: Use static properties (no rawValue initializer)

Common Patterns

Pattern 1: Required Int32 Parameters

Kotlin Definition:

data class RoundFormat(
    val numEnds: Int = 6,  // Required Int32
    val numArrows: Int = 3  // Required Int32
)

Swift Usage:

// ✅ CORRECT - Auto-converts from Int32
let roundFormat = RoundFormat(
    numEnds: Int32(truncating: (data["numEnds"] as? NSNumber) ?? 6),
    numArrows: Int32(truncating: (data["numArrows"] as? NSNumber) ?? 3)
)

Why This Works:

  • Swift Int32 automatically converts to Kotlin Int for required parameters
  • No need for KotlinInt wrapper

Source: TournamentListViewModel.swift:74-75


Pattern 2: Required Int64 Parameters

Kotlin Definition:

data class Tournament(
    val createdAt: Long = 0L,  // Required Int64
    val updatedAt: Long = 0L   // Required Int64
)

Swift Usage:

// ✅ CORRECT - Auto-converts from Int64
let tournament = Tournament(
    createdAt: (data["createdAt"] as? NSNumber)?.int64Value ?? 0,
    updatedAt: (data["updatedAt"] as? NSNumber)?.int64Value ?? 0
)

Why This Works:

  • Swift Int64 automatically converts to Kotlin Long for required parameters
  • .int64Value extracts Int64 from NSNumber

Source: TournamentListViewModel.swift:139-140


Pattern 3: Optional Int32 Parameters (THE TRICKY ONE)

Kotlin Definition:

data class RoundFormat(
    val timeLimit: Int? = null  // Optional Int32
)

Swift Usage - CORRECT:

// ✅ CORRECT - Must wrap in KotlinInt?
let roundFormat = RoundFormat(
    timeLimit: (data["timeLimit"] as? NSNumber).map { KotlinInt(int: Int32(truncating: $0)) }
)

Swift Usage - WRONG:

// ❌ WRONG - Compiler error
let roundFormat = RoundFormat(
    timeLimit: (data["timeLimit"] as? NSNumber).map { Int32(truncating: $0) }
)
// Error: Cannot convert value of type 'Int32?' to expected argument type 'KotlinInt?'

Why This Fails:

  • Optional Kotlin Int? maps to Swift KotlinInt? (not Int32?)
  • Compiler cannot infer nil handling for optional numeric types
  • Must explicitly wrap in KotlinInt object

The Pattern:

(data["field"] as? NSNumber).map { KotlinInt(int: Int32(truncating: $0)) }
//                              ^^^^^^^^^^^^^
//                              Required wrapper for optional Int

Source: TournamentListViewModel.swift:118


Pattern 4: Optional Int64 Parameters (ALSO TRICKY)

Kotlin Definition:

data class Tournament(
    val startTime: Long? = null,
    val endTime: Long? = null,
    val registrationDeadline: Long? = null
)

Swift Usage - CORRECT:

// ✅ CORRECT - Must wrap in KotlinLong?
let tournament = Tournament(
    startTime: (data["startTime"] as? NSNumber).map { KotlinLong(longLong: $0.int64Value) },
    endTime: (data["endTime"] as? NSNumber).map { KotlinLong(longLong: $0.int64Value) },
    registrationDeadline: (data["registrationDeadline"] as? NSNumber).map { KotlinLong(longLong: $0.int64Value) }
)

Swift Usage - WRONG:

// ❌ WRONG - Compiler error
let tournament = Tournament(
    startTime: (data["startTime"] as? NSNumber)?.int64Value
)
// Error: Cannot convert value of type 'Int64?' to expected argument type 'KotlinLong?'

The Pattern:

(data["field"] as? NSNumber).map { KotlinLong(longLong: $0.int64Value) }
//                              ^^^^^^^^^^^^^^^
//                              Required wrapper for optional Long

Source: TournamentListViewModel.swift:132-134


Pattern 5: String Parameters (Easy)

Kotlin Definition:

data class Tournament(
    val name: String = "",          // Required
    val description: String = "",   // Required
    val location: String? = null    // Optional
)

Swift Usage:

// ✅ CORRECT - Auto-converts for both required and optional
let tournament = Tournament(
    name: data["name"] as? String ?? "",
    description: data["description"] as? String ?? "",
    location: data["location"] as? String  // No wrapper needed
)

Why This Works:

  • Swift String and String? auto-convert to Kotlin String and String?
  • No special handling needed for optional strings

Source: TournamentListViewModel.swift:126-143


Pattern 6: Boolean Parameters (Easy)

Kotlin Definition:

data class Tournament(
    val isPublic: Boolean = true,        // Required
    val allowSpectators: Boolean = true  // Required
)

Swift Usage:

// ✅ CORRECT - Auto-converts for both required and optional
let tournament = Tournament(
    isPublic: data["public"] as? Bool ?? true,
    allowSpectators: data["allowSpectators"] as? Bool ?? true
)

Source: TournamentListViewModel.swift:136-147


Pattern 7: Array Parameters (Easy)

Kotlin Definition:

data class Tournament(
    val participantIds: List<String> = emptyList()  // Required
)

Swift Usage:

// ✅ CORRECT - Auto-converts
let tournament = Tournament(
    participantIds: data["participantIds"] as? [String] ?? []
)

Source: TournamentListViewModel.swift:138


Enum Handling

The Problem: No rawValue Initializer

Kotlin Enum Definition:

enum class TournamentStatus {
    OPEN,
    IN_PROGRESS,
    COMPLETED,
    CANCELLED,
    DELETED
}

Swift Usage - WRONG:

// ❌ WRONG - No such initializer exists
let status = TournamentStatus(rawValue: "OPEN")
// Error: 'TournamentStatus' has no member 'init(rawValue:)'

The Solution: Switch Statement Pattern

Swift Usage - CORRECT:

// ✅ CORRECT - Use switch statement
let statusString = data["status"] as? String ?? "OPEN"
let status: TournamentStatus = {
    switch statusString.uppercased() {
    case "OPEN": return .open
    case "IN_PROGRESS": return .inProgress
    case "COMPLETED": return .completed
    case "CANCELLED": return .cancelled
    case "DELETED": return .deleted
    default: return .open
    }
}()

Why This Works:

  • Kotlin enums expose static properties in Swift (.open, .inProgress, etc.)
  • Case conversion: IN_PROGRESS (Kotlin) → .inProgress (Swift)
  • Must manually map string values to enum cases

Source: TournamentListViewModel.swift:61-70


Complete Enum Conversion Examples

Distance Enum:

let distanceString = data["distance"] as? String ?? "EIGHTEEN_METERS"
let distance: Distance = {
    switch distanceString.uppercased() {
    case "EIGHTEEN_METERS": return .eighteenMeters
    case "SEVENTY_METERS": return .seventyMeters
    default: return .eighteenMeters
    }
}()

Source: TournamentListViewModel.swift:78-84

TargetSize Enum:

let targetSizeString = data["targetSize"] as? String ?? "FORTY_CM"
let targetSize: TargetSize = {
    switch targetSizeString.uppercased() {
    case "FORTY_CM": return .fortyCm
    case "HUNDRED_TWENTY_TWO_CM": return .hundredTwentyTwoCm
    default: return .fortyCm
    }
}()

Source: TournamentListViewModel.swift:87-93


Firestore NSNumber Handling

The Problem

Firestore returns numeric values as NSNumber, not native Swift integers. You must extract the appropriate type before using with Kotlin.

Required Int32 with NSNumber

Pattern:

// For required Int32 parameters
let value = Int32(truncating: (data["field"] as? NSNumber) ?? defaultValue)

Example:

let roundFormat = RoundFormat(
    numEnds: Int32(truncating: (data["numEnds"] as? NSNumber) ?? 6),
    numArrows: Int32(truncating: (data["numArrows"] as? NSNumber) ?? 3)
)

Steps:

  1. Cast to NSNumber? with as?
  2. Use Int32(truncating:) to convert NSNumber → Int32
  3. Provide default value with ??

Source: TournamentListViewModel.swift:74-75


Required Int64 with NSNumber

Pattern:

// For required Int64 parameters
let value = (data["field"] as? NSNumber)?.int64Value ?? defaultValue

Example:

let tournament = Tournament(
    createdAt: (data["createdAt"] as? NSNumber)?.int64Value ?? 0,
    updatedAt: (data["updatedAt"] as? NSNumber)?.int64Value ?? 0
)

Steps:

  1. Cast to NSNumber? with as?
  2. Extract Int64 with .int64Value
  3. Provide default value with ??

Source: TournamentListViewModel.swift:139-140


Optional Int32 with NSNumber

Pattern:

// For optional Int32 parameters (must wrap)
let value = (data["field"] as? NSNumber).map { KotlinInt(int: Int32(truncating: $0)) }

Example:

let roundFormat = RoundFormat(
    timeLimit: (data["timeLimit"] as? NSNumber).map { KotlinInt(int: Int32(truncating: $0)) }
)

Steps:

  1. Cast to NSNumber? with as?
  2. Use .map to transform if present
  3. Inside map: Convert NSNumber → Int32 → KotlinInt

Source: TournamentListViewModel.swift:118


Optional Int64 with NSNumber

Pattern:

// For optional Int64 parameters (must wrap)
let value = (data["field"] as? NSNumber).map { KotlinLong(longLong: $0.int64Value) }

Example:

let tournament = Tournament(
    startTime: (data["startTime"] as? NSNumber).map { KotlinLong(longLong: $0.int64Value) },
    endTime: (data["endTime"] as? NSNumber).map { KotlinLong(longLong: $0.int64Value) }
)

Steps:

  1. Cast to NSNumber? with as?
  2. Use .map to transform if present
  3. Inside map: Extract Int64 and wrap in KotlinLong

Source: TournamentListViewModel.swift:132-134


Troubleshooting

Common Error 1: Cannot Convert Int32? to KotlinInt?

Error Message:

Cannot convert value of type 'Int32?' to expected argument type 'KotlinInt?'

Cause: You’re passing a Swift optional integer to a Kotlin optional Int parameter without wrapping.

Wrong Code:

let roundFormat = RoundFormat(
    timeLimit: (data["timeLimit"] as? NSNumber).map { Int32(truncating: $0) }
)

Fix:

let roundFormat = RoundFormat(
    timeLimit: (data["timeLimit"] as? NSNumber).map { KotlinInt(int: Int32(truncating: $0)) }
)

Rule: Optional Kotlin Int? requires KotlinInt? wrapper in Swift.


Common Error 2: Cannot Convert Int64? to KotlinLong?

Error Message:

Cannot convert value of type 'Int64?' to expected argument type 'KotlinLong?'

Cause: You’re passing a Swift optional Int64 to a Kotlin optional Long parameter without wrapping.

Wrong Code:

let tournament = Tournament(
    startTime: (data["startTime"] as? NSNumber)?.int64Value
)

Fix:

let tournament = Tournament(
    startTime: (data["startTime"] as? NSNumber).map { KotlinLong(longLong: $0.int64Value) }
)

Rule: Optional Kotlin Long? requires KotlinLong? wrapper in Swift.


Common Error 3: TournamentStatus Has No Member init(rawValue:)

Error Message:

'TournamentStatus' has no member 'init(rawValue:)'

Cause: Kotlin enums don’t expose rawValue initializers in Swift like native Swift enums do.

Wrong Code:

let status = TournamentStatus(rawValue: "OPEN")

Fix:

let statusString = data["status"] as? String ?? "OPEN"
let status: TournamentStatus = {
    switch statusString.uppercased() {
    case "OPEN": return .open
    case "IN_PROGRESS": return .inProgress
    case "COMPLETED": return .completed
    default: return .open
    }
}()

Rule: Use switch statements to convert strings to Kotlin enums.


Common Error 4: Whack-a-Mole Compilation

Symptom:

  • Fix one type error
  • Build project
  • New type error appears
  • Repeat 10+ times

Cause: Xcode stops compilation at the first error. Each fix reveals the next error.

Solution:

  1. Read this entire guide before writing conversion code
  2. Understand required vs optional patterns upfront
  3. Write correct code the first time
  4. Use the quick reference table below

Quick Reference

Conversion Cheat Sheet

From Firestore NSNumber to Kotlin:

Kotlin TypeRequired ParameterOptional Parameter
IntInt32(truncating: nsNumber ?? default).map { KotlinInt(int: Int32(truncating: $0)) }
LongnsNumber?.int64Value ?? default.map { KotlinLong(longLong: $0.int64Value) }
Stringstring ?? defaultstring (no wrapper)
Booleanbool ?? defaultbool (no wrapper)
List<T>array ?? []array (no wrapper)
EnumSwitch statementSwitch statement

Remember:

  • Numeric optionals: Must wrap in KotlinInt? / KotlinLong?
  • Other optionals: Auto-convert, no wrapper
  • Enums: No rawValue, use switch

Best Practices

1. Create Helper Extensions

To reduce boilerplate, create extensions on Dictionary:

extension Dictionary where Key == String, Value == Any {
    func kotlinInt(for key: String, default: Int32 = 0) -> Int32 {
        return Int32(truncating: (self[key] as? NSNumber) ?? NSNumber(value: `default`))
    }
 
    func kotlinLong(for key: String, default: Int64 = 0) -> Int64 {
        return (self[key] as? NSNumber)?.int64Value ?? `default`
    }
 
    func kotlinIntOptional(for key: String) -> KotlinInt? {
        return (self[key] as? NSNumber).map { KotlinInt(int: Int32(truncating: $0)) }
    }
 
    func kotlinLongOptional(for key: String) -> KotlinLong? {
        return (self[key] as? NSNumber).map { KotlinLong(longLong: $0.int64Value) }
    }
}

Usage:

let tournament = Tournament(
    maxParticipants: data.kotlinInt(for: "maxParticipants", default: 50),
    startTime: data.kotlinLongOptional(for: "startTime"),
    createdAt: data.kotlinLong(for: "createdAt")
)

2. Use Kotlin Serialization for Future Work

Current Approach: Manual parsing (as documented in this guide)

Future Improvement: Consider using Kotlin Serialization with Swift Codable bridge if available

Benefit:

  • Automatic type conversion
  • Less boilerplate code
  • Type safety at compile time

Trade-off:

  • More complex setup
  • May not support all Firestore types
  • Manual parsing works and is explicit

Recommendation: Stick with manual parsing until KMP Firestore serialization is more mature.


3. Test iOS Explicitly

Lesson from Week 28: Android Firebase SDK and GitLive on iOS handle types differently. Always test both platforms.

Don’t assume:

  • “It works on Android” ≠ “It works on iOS”
  • Native SDKs may auto-convert types
  • Cross-platform wrappers may not

Always verify:

  • Build iOS framework after Kotlin changes
  • Run in Xcode simulator
  • Check console logs for data parsing issues

Real-World Example

Complete Tournament Parsing

This example from TournamentListViewModel.swift shows all patterns in one place:

let tournaments = documents.compactMap { doc -> Tournament? in
    let data = doc.data()
 
    // Required fields
    guard let id = data["id"] as? String,
          let name = data["name"] as? String else {
        return nil
    }
 
    // Enum conversion
    let statusString = data["status"] as? String ?? "OPEN"
    let status: TournamentStatus = {
        switch statusString.uppercased() {
        case "OPEN": return .open
        case "IN_PROGRESS": return .inProgress
        case "COMPLETED": return .completed
        default: return .open
        }
    }()
 
    // Nested object with Int32 fields
    let roundFormatData = data["roundFormat"] as? [String: Any] ?? [:]
    let roundFormat = RoundFormat(
        numEnds: Int32(truncating: (roundFormatData["numEnds"] as? NSNumber) ?? 6),    // Required Int32
        numArrows: Int32(truncating: (roundFormatData["numArrows"] as? NSNumber) ?? 3), // Required Int32
        distance: .eighteenMeters,  // Enum
        targetSize: .fortyCm,       // Enum
        scoringSystem: .standard10Ring,  // Enum
        allowScoringCorrections: roundFormatData["allowScoringCorrections"] as? Bool ?? true,  // Required Bool
        timeLimit: (roundFormatData["timeLimit"] as? NSNumber).map { KotlinInt(int: Int32(truncating: $0)) },  // Optional Int32 - MUST WRAP
        scoringMethod: .cumulative  // Enum
    )
 
    return Tournament(
        id: id,  // Required String
        name: name,  // Required String
        description: data["description"] as? String ?? "",  // Required String with default
        createdBy: data["createdBy"] as? String ?? "",  // Required String with default
        status: status,  // Enum
        maxParticipants: Int32(truncating: (data["maxParticipants"] as? NSNumber) ?? 50),  // Required Int32
        currentParticipants: Int32(truncating: (data["currentParticipants"] as? NSNumber) ?? 0),  // Required Int32
        startTime: (data["startTime"] as? NSNumber).map { KotlinLong(longLong: $0.int64Value) },  // Optional Long - MUST WRAP
        endTime: (data["endTime"] as? NSNumber).map { KotlinLong(longLong: $0.int64Value) },  // Optional Long - MUST WRAP
        registrationDeadline: (data["registrationDeadline"] as? NSNumber).map { KotlinLong(longLong: $0.int64Value) },  // Optional Long - MUST WRAP
        roundFormat: roundFormat,  // Nested object
        isPublic: data["public"] as? Bool ?? true,  // Required Bool
        joinCode: data["joinCode"] as? String ?? "",  // Required String
        participantIds: data["participantIds"] as? [String] ?? [],  // Required Array
        createdAt: (data["createdAt"] as? NSNumber)?.int64Value ?? 0,  // Required Long
        updatedAt: (data["updatedAt"] as? NSNumber)?.int64Value ?? 0,  // Required Long
        location: data["location"] as? String ?? "",  // Required String
        allowSpectators: data["allowSpectators"] as? Bool ?? true  // Required Bool
    )
}

Source: TournamentListViewModel.swift:50-176

Patterns Demonstrated:

  • ✅ Required String (auto-converts)
  • ✅ Required Int32 from NSNumber
  • ✅ Required Int64 from NSNumber
  • ✅ Optional Long → KotlinLong? wrapper
  • ✅ Optional Int → KotlinInt? wrapper
  • ✅ Enum with switch statement
  • ✅ Required Bool (auto-converts)
  • ✅ Required Array (auto-converts)
  • ✅ Nested object construction

Result: 162 lines of manual parsing code that handles all type conversions correctly.


Week 28 iOS Firebase Integration:

KMP Architecture:

External Resources:


Key Takeaways

  1. Required vs Optional: Numeric types have different conversion rules

    • Required: Auto-converts (Int32, Int64)
    • Optional: Must wrap (KotlinInt?, KotlinLong?)
  2. Enums: No rawValue initializer, use switch statements

  3. Firestore NSNumber: Must extract appropriate type before converting

  4. Whack-a-mole Debugging: Understand patterns upfront to avoid repetitive errors

  5. This is a One-Time Learning Cost: Once documented, future developers avoid hours of debugging

  6. Pattern Applies Universally: These rules apply to ALL Kotlin models exposed to Swift, not just Firebase integration


Status: Complete - All patterns extracted from production code

Source: iosApp/ArcheryApprentice/ArcheryApprentice/TournamentListViewModel.swift (Week 28 iOS Firebase integration)

Impact: Future iOS developers can reference this guide instead of discovering patterns through trial and error


Additional Patterns from iOS Phase 2 (Week 28)

During iOS Phase 2 implementation (TournamentDetailView/ViewModel), additional type conversion patterns were discovered that complement the Firebase parsing patterns above.

Pattern: Reading from Kotlin Types (Kotlin → Swift)

When to Use: Displaying Kotlin data in Swift UI

KotlinLong → Swift:

// ✅ CORRECT - Use .int64Value property
let tournament: Tournament = viewModel.tournament
if let startTime: KotlinLong = tournament.startTime {
    let date = Date(timeIntervalSince1970: Double(startTime.int64Value) / 1000.0)
}
 
// ❌ WRONG - No .longValue property
let date = Date(timeIntervalSince1970: Double(startTime.longValue) / 1000.0)
// Error: Value of type 'KotlinLong?' has no member 'longValue'

KotlinInt → Swift:

// ✅ CORRECT - Use .int32Value property
let numEnds: KotlinInt = roundFormat.numEnds
let endsCount = Int(numEnds.int32Value)
 
// ❌ WRONG - No .intValue property
let endsCount = Int(numEnds.intValue)
// Error: Value of type 'KotlinInt' has no member 'intValue'

Pattern Summary:

KotlinInt   .int32Value  Int32 Int (if needed)
KotlinLong  .int64Value  Int64 Int (if needed)

Source: TournamentDetailView.swift:107, iOS Phase 2


Pattern: Swift Int to Kotlin Int32 (Calling Kotlin Functions)

When to Use: Passing Swift Int values to Kotlin functions/constructors

Scenario: Swift reads data as Int, but Kotlin function expects Int32

// Firebase/local storage returns Int
let currentParticipants: Int = data["currentParticipants"] as? Int ?? 0
let maxParticipants: Int = data["maxParticipants"] as? Int ?? 50
 
// Kotlin constructor expects Int32
let tournament = Tournament(
    // ✅ CORRECT - Wrap Swift Int in Int32()
    currentParticipants: Int32(currentParticipants),
    maxParticipants: Int32(maxParticipants)
)
 
// ❌ WRONG - Direct Int assignment
let tournament = Tournament(
    currentParticipants: currentParticipants,
    maxParticipants: maxParticipants
)
// Error: Cannot convert value of type 'Int' to expected argument type 'Int32'

Why This Happens:

  • Swift Int is platform word size (64-bit on modern iOS)
  • Kotlin Int is always 32-bit
  • No automatic conversion from larger to smaller type
  • Must explicitly downcast with Int32()

Pattern: Int32(swiftIntValue)

Source: TournamentDetailViewModel.swift:298-299, 313, iOS Phase 2


Pattern: Property Name Mappings (Kotlin → Swift)

When to Use: Accessing Kotlin properties from Swift

Kotlin Boolean Properties:

// Kotlin: var isPublic: Boolean = true
// Swift: isPublic (no change)
if tournament.isPublic {
    // ✅ CORRECT
}
 
// ❌ WRONG - Don't add underscore for 'is' prefix
if tournament.isPublic_ {
    // Error: Value of type 'Tournament' has no member 'isPublic_'
}

Kotlin Properties with Swift Keyword Collisions:

// Kotlin: var public: Boolean = true
// Swift: public_ (underscore added automatically)
if tournament.public_ {
    // ✅ CORRECT for keyword collision
}
 
// ❌ WRONG - 'public' is a Swift keyword
if tournament.public {
    // Error: Expected member name following '.'
}

Pattern Summary:

Kotlin Property          Swift Access
-------------------      ---------------
isPublic: Boolean    →   isPublic
isActive: Boolean    →   isActive
public: Boolean      →   public_  (keyword collision)
private: Boolean     →   private_ (keyword collision)

Rule: Only add underscore suffix when Kotlin property name is a Swift keyword

Source: TournamentDetailView.swift:181, iOS Phase 2


Pattern: Guard Let vs Nil Check (Swift Compiler Warnings)

When to Use: Checking for nil without using the unwrapped value

Scenario: You need to return early if a value is nil, but don’t need to use the unwrapped value

// ❌ COMPILER WARNING - Unused binding
guard let tournament = tournament else {
    errorMessage = "Tournament not loaded"
    return
}
// Warning: Immutable value 'tournament' was never used
 
// ✅ CORRECT - Use nil check instead of guard let
guard tournament != nil else {
    errorMessage = "Tournament not loaded"
    return
}
// No warning, clearer intent

Why This Matters:

  • If you don’t use the unwrapped binding, you’re cluttering the scope
  • Swift compiler warns about unused immutable values
  • != nil check is clearer when you only care about existence

When to Use Guard Let:

// ✅ CORRECT - Use guard let when you need the unwrapped value
guard let tournament = tournament else { return }
 
// Now use tournament safely
print(tournament.name)
tournamentRepository.update(tournament)

When to Use Nil Check:

// ✅ CORRECT - Use nil check when you only need to verify existence
guard tournament != nil else {
    showError()
    return
}
// No need for unwrapped value, just checking it exists

Source: TournamentDetailViewModel.swift:68, 139, iOS Phase 2


Last Updated: 2025-11-18 Week: 28 Related PRs:

  • #266 (iOS Firebase integration with real tournament data)
  • #275 (TournamentDetailViewModel - iOS Phase 2)
  • #276 (TournamentDetailView - iOS Phase 2)
  • #278 (iOS Phase 2 build error fixes)