Why this happens
The snippet from the Swift book is not a file I/O example at all. It demonstrates @lazy (now lazy) stored properties: DataImporter is only created the first time manager.importer is touched. Nothing in it opens, reads or writes a file, which is why copying it gets you nowhere.
The second thing that trips people up is location. An app runs inside a sandbox, and the folder your code is compiled into — the bundle — is read-only on a real device. Writing to Bundle.main.bundlePath fails silently or throws. You need a writable directory, and the Documents directory is the standard choice for user-visible files.
The third issue is age. The old APIs (writeToFile(_:atomically:encoding:error:), NSString.stringWithContentsOfFile) no longer exist. Modern Swift uses URL plus throwing methods on String.
How to fix it
1. Write and read in the Documents directory
This is the direct replacement for the book sample. It works in a playground on macOS and in an iOS app.
import Foundation
let fileName = "data.txt"
let text = "Hello World in Swift Language."
let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let fileURL = documents.appendingPathComponent(fileName)
// Write
do {
try text.write(to: fileURL, atomically: true, encoding: .utf8)
} catch {
print("Write failed: \(error)")
}
// Read back
do {
let loaded = try String(contentsOf: fileURL, encoding: .utf8)
print(loaded)
} catch {
print("Read failed: \(error)")
}
Two details matter here. atomically: true writes to a temporary file first and then swaps it in, so a crash mid-write cannot leave you with a half-written file. And encoding: .utf8 is explicit — never rely on the encoding-less overloads, which guess and can misread non-ASCII text.
If you want the file inside a subfolder, create the folder first, because write(to:) does not create intermediate directories:
try FileManager.default.createDirectory(at: folderURL,
withIntermediateDirectories: true)
2. Read a file that ships inside the app
If data.txt is part of your target (drag it into the project navigator and tick Copy items if needed and your app target under Add to targets), read it from the bundle. Prefer the URL API over path(forResource:) so you never force-unwrap a path string:
import Foundation
if let url = Bundle.main.url(forResource: "data", withExtension: "txt") {
do {
let contents = try String(contentsOf: url, encoding: .utf8)
print(contents)
} catch {
print("Could not read bundled file: \(error)")
}
} else {
print("data.txt is not in the bundle")
}
If this branch prints “not in the bundle”, the file is missing from Copy Bundle Resources in Build Phases, or it was added to the project but not to the app target.
3. Do not swallow the error
The accepted answer’s catch { /* error handling here */ } is fine as a placeholder, but in real code an empty catch hides the two failures you will actually hit: the directory does not exist, or the file is not there yet. Log the error, or let it propagate:
func loadText(at url: URL) throws -> String {
try String(contentsOf: url, encoding: .utf8)
}
Avoid try? for reads you care about — it turns every failure into nil and you lose the reason.
4. Copy a bundled file to Documents before editing it
A common pattern is shipping a default file and letting the user modify it. Since the bundle is read-only, copy it once on first launch:
let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let destination = documents.appendingPathComponent("data.txt")
if !FileManager.default.fileExists(atPath: destination.path),
let source = Bundle.main.url(forResource: "data", withExtension: "txt") {
try FileManager.default.copyItem(at: source, to: destination)
}
After that, all reads and writes go to destination.
How to prevent it
- Never write into
Bundle.main.bundlePathorBundle.main.resourcePath— the bundle is read-only on device. - Build paths with
URL.appendingPathComponent(_:), not string concatenation with"/". - Always pass
encoding: .utf8explicitly on both read and write. - Use
atomically: truewhen overwriting a file the app may read again. - Handle the
throwsfromwrite(to:)andString(contentsOf:); an emptycatchwill cost you an hour of debugging later. - For files shared with an extension or another app, use
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier:)instead of the Documents directory.
FAQ
Where does the file actually end up on the device?
Inside the app’s sandbox, under Documents. On the Simulator you can open it from Finder via the app container. On a device it is only reachable through Xcode’s Devices window or the Files app if you enable file sharing in Info.plist.
Why does Xcode warn that String(contentsOfFile:) is deprecated?
The overload without an encoding guesses the encoding from the bytes, which is unreliable. Use String(contentsOf: url, encoding: .utf8) instead — it is the supported form and it throws a real error when the file is missing.
Can I read a file from the bundle and then write back to the same path?
No. The bundle is signed and read-only at runtime. Copy the file to the Documents directory on first launch and read and write that copy from then on.
Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.
