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 Type | Swift Required Parameter | Swift Optional Parameter |
|---|---|---|
Int (Int32) | Int32 (auto-converts) | KotlinInt? (must wrap) |
Long (Int64) | Int64 (auto-converts) | KotlinLong? (must wrap) |
String | String (auto-converts) | String? (auto-converts) |
Boolean | Bool (auto-converts) | Bool? (auto-converts) |
List<T> | [T] (auto-converts) | [T]? (auto-converts) |
| Enum | Static 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
rawValueinitializer)
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
Int32automatically converts to KotlinIntfor required parameters - No need for
KotlinIntwrapper
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
Int64automatically converts to KotlinLongfor required parameters .int64Valueextracts 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 SwiftKotlinInt?(notInt32?) - Compiler cannot infer nil handling for optional numeric types
- Must explicitly wrap in
KotlinIntobject
The Pattern:
(data["field"] as? NSNumber).map { KotlinInt(int: Int32(truncating: $0)) }
// ^^^^^^^^^^^^^
// Required wrapper for optional IntSource: 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 LongSource: 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
StringandString?auto-convert to KotlinStringandString? - 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:
- Cast to
NSNumber?withas? - Use
Int32(truncating:)to convert NSNumber → Int32 - 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 ?? defaultValueExample:
let tournament = Tournament(
createdAt: (data["createdAt"] as? NSNumber)?.int64Value ?? 0,
updatedAt: (data["updatedAt"] as? NSNumber)?.int64Value ?? 0
)Steps:
- Cast to
NSNumber?withas? - Extract Int64 with
.int64Value - 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:
- Cast to
NSNumber?withas? - Use
.mapto transform if present - 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:
- Cast to
NSNumber?withas? - Use
.mapto transform if present - 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:
- Read this entire guide before writing conversion code
- Understand required vs optional patterns upfront
- Write correct code the first time
- Use the quick reference table below
Quick Reference
Conversion Cheat Sheet
From Firestore NSNumber to Kotlin:
| Kotlin Type | Required Parameter | Optional Parameter |
|---|---|---|
Int | Int32(truncating: nsNumber ?? default) | .map { KotlinInt(int: Int32(truncating: $0)) } |
Long | nsNumber?.int64Value ?? default | .map { KotlinLong(longLong: $0.int64Value) } |
String | string ?? default | string (no wrapper) |
Boolean | bool ?? default | bool (no wrapper) |
List<T> | array ?? [] | array (no wrapper) |
| Enum | Switch statement | Switch 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.
Related Documentation
Week 28 iOS Firebase Integration:
- Week 28: iOS Firebase Integration Milestone - Documents the @SerialName fix and overall milestone
KMP Architecture:
- KMP Migration Architecture - Overall KMP architecture
- Platform Abstractions Status - Cross-platform abstractions
- Week 17: iOS ViewModels - iOS ViewModel patterns
External Resources:
- Kotlin/Native Interop - Official Kotlin/Swift interop documentation
- KMP-NativeCoroutines - Flow to AsyncSequence conversion
- GitLive Firebase SDK - Cross-platform Firebase for KMP
Key Takeaways
-
Required vs Optional: Numeric types have different conversion rules
- Required: Auto-converts (Int32, Int64)
- Optional: Must wrap (KotlinInt?, KotlinLong?)
-
Enums: No rawValue initializer, use switch statements
-
Firestore NSNumber: Must extract appropriate type before converting
-
Whack-a-mole Debugging: Understand patterns upfront to avoid repetitive errors
-
This is a One-Time Learning Cost: Once documented, future developers avoid hours of debugging
-
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
Intis platform word size (64-bit on modern iOS) - Kotlin
Intis 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 intentWhy This Matters:
- If you don’t use the unwrapped binding, you’re cluttering the scope
- Swift compiler warns about unused immutable values
!= nilcheck 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 existsSource: 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)