Notes on Swift From Building an iOS Game
August 3, 2025 Update: The game is now available! For the past few months I've been working on an iOS game. It's almost finished now but I still would like to polish some aspects a bit more and look into what's happening with iOS 26 and the new Games app before I publish it, to ensure things works well for everyone. I don't want to give too many details just yet, but I can say it's a puzzle/card game. I will post more details when the game is available (hopefully within the month), stay tuned! I think Swift is a fantastic language. Every new aspect I explored felt well-thought-out by the language designers. I was also pleasantly surprised by some smaller features that, while probably inconsequential in the grand scheme, I found neat, like argument labels, which as far as I know, are unique to Swift. The idea is that argument labels can enhance code readability because they allow descriptive external names that clarify the purpose of parameters when calling the function. This makes some function calls a bit easier to understand and more self-documenting. By separating external and internal names, I could use concise internal names within the function body while maintaining names that explain the context from where it's called . It's pretty neat! Below are some of the features and patterns that really stood out to me. I'm sure some of these exist in other languages or frameworks but I don't think I've seen them done exactly this way. I found myself using enums far more extensively than I've done before in other languages, especially with their advanced capabilities. Using String raw values for enums was incredibly practical. It allowed for type-safe references to asset names without relying on error-prone magic strings. The compiler catches typos, making changes quick and easy. It was also very nice to be able to represent different data types within a single enum. For Swift's switch statements, especially with enums that have associated values, enforce exhaustive handling. If I added a new For properties like This pattern of using I used computed properties for things like Using computed properties in classes to read from and write to These are fundamental for safe handling of optional values and significantly improve code readability. Optional chaining (?) allows for concise and safe access to properties or methods on optional values. If any part of the chain is nil, the entire expression evaluates to nil without crashing. This is far cleaner than nested if let statements for simple access. Nil coalescing (??) is a clean way to provide a default value if an optional is nil. Together, optional chaining and nil coalescing drastically reduce the boilerplate associated with nil checks. It's much nicer to have code that's direct and easier to follow, especially when coming back to a function after a few weeks. Understanding and correctly applying weak and unowned was critical for memory management, especially with closures and SpriteKit animation-related code blocks. I faced some trouble with retain cycles, obviously. In SpriteKit for example, with nodes, timers, and completion handlers, it's easy to create strong reference cycles that lead to memory leaks and crashes that are hard to track. What's more annoying is that these crashes did not happen in the simulator, but only on real and slightly older hardware and never with dev builds. Using Building this game was a lot of fun, if you're interested in checking it out, check back here in a month or two, currently waiting for the tax agency in Sweden to finish my paper work.func sendMessage(to recipient: String, content message: String) {
print("Sending '\(message)' to \(recipient).")
}
sendMessage(to: "Alice", content: "Hello, how are you?")
Enums
enum MusicTrack: String {
case mainMenuMusic = "MainMenu.mp3"
case gameplayMusic = "GameplayLoop.mp3"
}
let trackName = MusicTrack.mainMenuMusic.rawValue // "MainMenu.mp3"
CardType
for example, having cases like .number(String)
and .powerup(PowerupType)
meant that each card type could carry its specific data directly. This meant that I didn't need complex class hierarchies or multiple optional properties. It's much cleaner this way.enum PowerupType: String, Codable {
case extraTime = "Extra Time"
case scoreMultiplier = "Score Multiplier"
}
enum CardType: Codable, Equatable {
case number(String)
case powerup(PowerupType)
}
let myCard = CardType.number("7")
let powerCard = CardType.powerup(.scoreMultiplier)
PowerupType
, the compiler would immediately flag all switch statements that needed updating. This compile-time guarantee was very useful for preventing runtime bugs easily and made it simple for me to work on various aspects of the game in small chunks. When I introduced PowerCard
s I didn't come up with all the ideas I wanted to do at once, and I wouldn't have wanted to write them then anyway. It was very nice that I could add a new power card type to the enum whenever I get a new idea then have Xcode point out all the places I needed to update.func handleCard(_ card: CardType) {
switch card {
case .number(let value):
print("Number card with value: \(value)")
case .powerup(let type):
print("Powerup card of type: \(type.rawValue)")
}
}
Properties
MusicManager.volume
or isMuted
, the didSet
observer allowed me to automatically persist the new value to UserDefaults
and trigger an update to the audio player. This pattern keeps the logic for reacting to a property change directly co-located with the property itself.didSet
observers also helped encapsulate the "what happens when this changes" logic, which reduced the need for explicit method calls every time a property is modified. This made the code more self-contained and easier to keep up to date as I progressed, for example, or changed how scoring works.class MusicManager {
var volume: Float = 0.5 {
didSet {
UserDefaults.standard.set(volume, forKey: "musicVolume")
updatePlayerVolume()
}
}
private func updatePlayerVolume() {
// Logic to set actual player volume
print("Volume set to \(volume)")
}
}
let manager = MusicManager()
manager.volume = 0.7 // didSet is automatically called here
MusicManager.isPlaying
(derived from the underlying AVAudioPlayer
's state) and currentTrackName
(derived from currentTrack
) to prevent redundant storage and ensure consistency. The value is always fresh.class MusicManager {
var musicPlayer: AVAudioPlayer? // Assume this exists
var currentTrack: MusicTrack?
var isPlaying: Bool {
return musicPlayer?.isPlaying ?? false
}
var currentTrackName: String? {
return currentTrack?.rawValue
}
}
UserDefaults
was particularly effective. It made persistent data feel like a regular stored property, simplifying access and modification while handling the UserDefaults
interaction internally.struct Stats {
static var cardsPlayed: Int {
get {
return UserDefaults.standard.integer(forKey: "cardsPlayedCount")
}
set {
UserDefaults.standard.set(newValue, forKey: "cardsPlayedCount")
}
}
}
// Usage:
Stats.cardsPlayed += 1 // Reads, increments, writes back
Optional Chaining (?) and Nil Coalescing (??)
var playerNode: SKNode? // Could be nil
var gameScene: SKScene? // Could be nil
// Optional chaining:
playerNode?.position.x = 100 // Only sets if playerNode is not nil
gameScene?.presentScene(SKScene()) // Only presents if gameScene is not nil
// Nil coalescing, scoreText default to "0" if totalStatsData is nil
let scoreText = totalStatsData?.getFormattedScore() ?? "0"
weak and unowned References
[weak self]
in capture lists for (like Timer
callbacks or SKAction
completion blocks) is good for safer access to various variables in closures (with a guard let self = self else { return })
for example). If self
is deallocated, the weak
reference becomes nil
.class GameScene: SKScene {
var gameTimer: Timer?
func setupTimer() {
gameTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in
// Safely unwrap self. If GameScene is deallocated, self will be nil.
guard let self = self else {
timer.invalidate() // Stop the timer if scene is gone
return
}
self.updateGameTime() // Safely call method on self
}
}
func updateGameTime() {
// game time update logic
print("Game time updated.")
}
}