Swift
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
72: FileManager for File System Access
When you first start dealing with the file system in Swift, it's tempting to treat it like a simple folder structure on your desktop. You might think, "I'll just create a string for the path, tack on a filename, and tell the system to write the data there." I've seen a lot of developers start this way, and while it might seem to work on a simulator or a specific Mac, it's a recipe for disaster once your app hits a real device.
The Danger of Hardcoded Strings
The naive approach usually looks something like this. You define a path as a string, perhaps trying to guess where the Documents folder is, and then you try to write to it using basic string concatenation:
let path = "/Users/username/Documents/settings.json"
let data = "{\"theme\": \"dark\"}".data(using: .utf8)
FileManager.default.createFile(atPath: path, contents: data, attributes: nil)
Here is the problem: on iOS and sandboxed macOS apps, you have absolutely no idea what the actual absolute path to your documents folder is. The system generates a unique UUID for your app's container every time it's installed or updated. If you hardcode a path, your code will fail the moment it leaves your machine. Even worse, using strings for paths is brittle. You have to manually manage slashes and worry about encoding, which is just asking for a bug that only appears on one specific OS version.
Leveraging the System's Sandbox Intelligence
The professional way to handle this is to stop thinking in strings and start thinking in URLs. In Swift, URL isn't just for websites; it's the primary way we navigate the local file system. Instead of guessing where your files live, you ask the FileManager to give you the correct, system-sanctioned directory for your specific app instance.
I always recommend using urls(for:in:). It returns an array, but since we're usually looking for a single system directory, we just grab the first one. Once you have that base URL, you use appendingPathComponent to build your file path. This ensures the system handles the slashes and directory delimiters correctly, regardless of the platform.
let fileManager = FileManager.default
do {
let documentsURL = try fileManager.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
let fileURL = documentsURL.appendingPathComponent("settings.json")
let content = "{\"theme\": \"dark\"}"
try content.write(to: fileURL, atomically: true, encoding: .utf8)
} catch {
print("File system error: \(error)")
}
Safety and Atomicity
You'll notice I used atomically: true in the example above. This is a small detail that separates the juniors from the seniors. If you write a file non-atomically and the app crashes (or the phone dies) halfway through the process, you end up with a corrupted, half-written file. That's a nightmare to debug.
When you set atomically to true, Swift writes the data to a temporary file first. Only once the write is successful does it swap the temporary file with the real one. It's a "all or nothing" approach. It costs a tiny bit of extra disk I/O, but the peace of mind knowing you won't corrupt your user's save data is well worth the trade-off.
One last thing: always wrap your FileManager logic in a do-catch block. The file system is volatile. Disk space can run out, permissions can change, or a file you expect to be there might have been deleted by the system during a cleanup cycle. If you use try! here, you're essentially gambling with your app's stability.
📋 Practical Task
Building a Persistent Game-State Saver
Create a small system that manages a player's progress. You need to implement a GameStateManager class that does the following:
- Defines a
GameStatestruct (containing aplayerName: StringandlevelReached: Int) that conforms toCodable. - Implements a
saveGame(state: GameState)method that encodes the state to JSON and writes it to a file namedplayer_progress.jsonin the app's Documents directory. Ensure the write is atomic. - Implements a
loadGame()method that reads the file from the Documents directory, decodes the JSON, and returns theGameStateobject. - Handles errors gracefully using
do-catch, returningnilor a default state if the file doesn't exist yet.
Test your implementation by saving a state, changing the value in memory, and then loading it back from the disk to verify the persistence works.
There are no comments for now.