Instruction file imported from sethdford/lag-secret-assassin-ios (
.cursor/rules/swift-best-practices.mdc). Copyright stays with the author.
Swift Best Practices (Lickability-Based)
Based on Lickability's Swift Best Practices
Code Organization
-
File Structure:
- Group related functionality together
- Use MARK comments to organize code sections
- Follow consistent import ordering: Foundation, UIKit, third-party, local modules
- Place extensions in separate files when they add significant functionality
-
Access Control:
- Use
privateby default, escalate tofileprivate,internal,publicas needed - Prefer
private(set)for properties that should be readable but not writable externally - Use
finalfor classes that shouldn't be subclassed
- Use
// ✅ DO: Proper access control
final class GameManager {
private let networkService: NetworkService
private(set) var currentGame: Game?
public init(networkService: NetworkService) {
self.networkService = networkService
}
}
// ❌ DON'T: Overly permissive access
class GameManager {
var networkService: NetworkService
var currentGame: Game?
}
Naming Conventions
- Variables and Functions:
- Use descriptive, self-documenting names
- Prefer clarity over brevity
- Use camelCase for variables, functions, and properties
- Use PascalCase for types, protocols, and enums
// ✅ DO: Clear, descriptive naming
func calculateDistanceBetweenPlayers(_ player1: Player, _ player2: Player) -> CLLocationDistance
var isUserWithinSafeZone: Bool
let maximumGameDuration: TimeInterval
// ❌ DON'T: Abbreviated or unclear names
func calcDist(_ p1: Player, _ p2: Player) -> CLLocationDistance
var inSafe: Bool
let maxDur: TimeInterval
- Constants and Static Properties:
- Use descriptive names that explain the purpose
- Group related constants in enums or structs
// ✅ DO: Well-organized constants
enum GameConstants {
static let defaultSafeZoneRadius: CLLocationDistance = 100.0
static let maximumPlayersPerGame = 20
static let gameTimeoutInterval: TimeInterval = 3600
}
// ❌ DON'T: Magic numbers or unclear constants
let radius = 100.0
let maxPlayers = 20
Optionals and Safety
- Optional Handling:
- Prefer safe unwrapping over force unwrapping
- Use guard statements for early returns
- Use nil coalescing operator when appropriate
// ✅ DO: Safe optional handling
guard let user = currentUser else {
logger.error("No current user available")
return
}
let displayName = user.displayName ?? "Anonymous"
// ❌ DON'T: Force unwrapping
let user = currentUser!
let displayName = user.displayName!
- Implicitly Unwrapped Optionals:
- Avoid unless absolutely necessary (IBOutlets, dependency injection)
- Document why they're needed when used
Error Handling
- Use Result Types:
- Prefer Result<Success, Failure> for async operations
- Use descriptive error types
// ✅ DO: Proper error handling
enum GameError: Error, LocalizedError {
case playerNotFound
case gameNotActive
case networkUnavailable
var errorDescription: String? {
switch self {
case .playerNotFound:
return "Player not found in current game"
case .gameNotActive:
return "No active game session"
case .networkUnavailable:
return "Network connection unavailable"
}
}
}
func joinGame(with code: String) -> Result<Game, GameError> {
// Implementation
}
SwiftUI Best Practices
- View Composition:
- Break down complex views into smaller, reusable components
- Use ViewBuilder for conditional content
- Prefer computed properties over functions for simple transformations
// ✅ DO: Composable views
struct GameStatusView: View {
let game: Game
var body: some View {
VStack(spacing: 16) {
gameHeaderView
playerListView
gameActionsView
}
}
@ViewBuilder
private var gameHeaderView: some View {
HStack {
Text(game.name)
.font(.headlineLarge)
Spacer()
GameStatusBadge(status: game.status)
}
}
}
- State Management:
- Use @State for local view state
- Use @StateObject for view-owned objects
- Use @ObservedObject for externally-owned objects
- Prefer @Published properties in ObservableObject
// ✅ DO: Proper state management
class GameViewModel: ObservableObject {
@Published var currentGame: Game?
@Published var isLoading = false
@Published var errorMessage: String?
private let gameService: GameService
init(gameService: GameService) {
self.gameService = gameService
}
}
struct GameView: View {
@StateObject private var viewModel = GameViewModel(gameService: GameService())
@State private var showingJoinSheet = false
}
Protocols and Extensions
- Protocol Design:
- Keep protocols focused and cohesive
- Use protocol extensions for default implementations
- Prefer composition over inheritance
// ✅ DO: Focused protocols
protocol GameEventHandler {
func handlePlayerJoined(_ player: Player)
func handlePlayerLeft(_ player: Player)
func handleGameEnded()
}
extension GameEventHandler {
func handlePlayerLeft(_ player: Player) {
// Default implementation
print("Player \(player.name) left the game")
}
}
- Extensions:
- Use extensions to organize code by functionality
- Place protocol conformance in separate extensions
- Use MARK comments to organize extension sections
// ✅ DO: Organized extensions
extension GameManager {
// MARK: - Game Lifecycle
func startGame() { /* implementation */ }
func endGame() { /* implementation */ }
}
extension GameManager: GameEventHandler {
// MARK: - GameEventHandler
func handlePlayerJoined(_ player: Player) {
// implementation
}
}
Documentation and Comments
- Documentation:
- Use /// for public APIs
- Include parameter descriptions and return values
- Document complex algorithms or business logic
/// Calculates the distance between two players and determines if they're within elimination range
/// - Parameters:
/// - hunter: The player attempting elimination
/// - target: The player being targeted
/// - Returns: The distance in meters, or nil if location data is unavailable
func calculateEliminationDistance(hunter: Player, target: Player) -> CLLocationDistance? {
// Implementation
}
- Comments:
- Use comments to explain "why", not "what"
- Keep comments up-to-date with code changes
- Use TODO comments with issue links (enforced by SwiftLint)
// ✅ DO: Explain the reasoning
// We cache the user's location for 30 seconds to reduce battery drain
// while still maintaining reasonable accuracy for gameplay
private let locationCacheTimeout: TimeInterval = 30
// TODO: Implement push notification retry logic - https://github.com/org/repo/issues/123
Testing Considerations
- Testable Code:
- Use dependency injection
- Avoid static dependencies
- Make methods pure when possible
// ✅ DO: Testable design
class LocationService {
private let locationManager: CLLocationManager
init(locationManager: CLLocationManager = CLLocationManager()) {
self.locationManager = locationManager
}
}
// ❌ DON'T: Hard dependencies
class LocationService {
private let locationManager = CLLocationManager()
}
Performance Guidelines
- Memory Management:
- Use weak references to break retain cycles
- Prefer value types (structs) when appropriate
- Be mindful of capture lists in closures
// ✅ DO: Proper memory management
class GameViewController: UIViewController {
private var gameService: GameService?
override func viewDidLoad() {
super.viewDidLoad()
gameService?.onGameUpdate = { [weak self] game in
self?.updateUI(with: game)
}
}
}
- Lazy Loading:
- Use lazy properties for expensive computations
- Consider lazy loading for UI components
// ✅ DO: Lazy initialization
class GameMapView: UIView {
private lazy var mapView: MKMapView = {
let map = MKMapView()
map.delegate = self
map.showsUserLocation = true
return map
}()
}