I/O
Swift’s I/O surface is built primarily on Foundation — the cross-platform framework for files, paths, and network operations. The principal types: FileManager (file system operations), URL (file and remote URLs), Data (byte container), String (text I/O), FileHandle (low-level file access), Pipe (process I/O), URLSession (HTTP). For console I/O, print() and readLine() admit substantial conventional uses; FileHandle.standardInput / .standardOutput / .standardError admit substantial control. Swift 5.5+ admits async file I/O through URL.resourceBytes and similar, integrating with the structured concurrency model. The combination — Foundation’s substantial file APIs, URL-based addressing, async byte streams (5.5+), the substantial String/Data conversion — is the substance of Swift’s I/O surface.
Console I/O
print("Hello") // print + newline
print("a", "b", "c") // "a b c"
print("a", "b", "c", separator: "-") // "a-b-c"
print("Hello", terminator: "") // no newline
print("error", to: &FileHandle.standardError) // to stderr (rare; conventional alternative below)
// Read a line:
if let line = readLine() {
print("got: \(line)")
}
// Read with prompt:
print("Enter name: ", terminator: "")
if let name = readLine() {
print("Hello, \(name)")
}
Standard streams
import Foundation
FileHandle.standardInput
FileHandle.standardOutput
FileHandle.standardError
// Write to stderr:
let stderr = FileHandle.standardError
let data = "error message\n".data(using: .utf8)!
stderr.write(data)
// Or use the conventional print to stderr:
struct StderrStream: TextOutputStream {
func write(_ string: String) {
FileHandle.standardError.write(string.data(using: .utf8)!)
}
}
var stderr = StderrStream()
print("error", to: &stderr)
File reading
The principal forms:
import Foundation
let url = URL(filePath: "/path/to/file.txt")
// Whole file as String:
let content = try String(contentsOf: url, encoding: .utf8)
// Whole file as Data:
let data = try Data(contentsOf: url)
// Whole file as bytes:
let bytes: [UInt8] = Array(data)
For substantial files, streaming via FileHandle:
let handle = try FileHandle(forReadingFrom: url)
defer { try? handle.close() }
while let chunk = try handle.read(upToCount: 1024), !chunk.isEmpty {
process(chunk)
}
Async file reading (Swift 5.5+)
import Foundation
let url = URL(filePath: "/path/to/file.txt")
// Async sequence of bytes:
let (bytes, _) = try await URLSession.shared.bytes(from: url)
// As lines:
for try await line in bytes.lines {
print(line)
}
// As characters:
for try await char in bytes.characters {
print(char)
}
// Or via FileHandle:
let handle = try FileHandle(forReadingFrom: url)
for try await line in handle.bytes.lines {
process(line)
}
The async forms admit substantial integration with the structured concurrency model; treated in Concurrency.
File writing
let url = URL(filePath: "/path/to/output.txt")
// Whole file:
try "content".write(to: url, atomically: true, encoding: .utf8)
try data.write(to: url)
// Append:
let handle = try FileHandle(forWritingTo: url)
try handle.seekToEnd()
try handle.write(contentsOf: "more content\n".data(using: .utf8)!)
try handle.close()
The atomically: true admits writing to a temporary file and renaming — admits substantial write atomicity.
For appending without seekToEnd() for new files, the conventional creation-or-append:
extension URL {
func append(_ data: Data) throws {
if let handle = try? FileHandle(forWritingTo: self) {
try handle.seekToEnd()
try handle.write(contentsOf: data)
try handle.close()
} else {
try data.write(to: self)
}
}
}
FileManager
The principal file-system API:
import Foundation
let fm = FileManager.default
// Existence:
fm.fileExists(atPath: path)
fm.fileExists(atPath: path, isDirectory: &isDir)
// Creation:
try fm.createDirectory(at: url, withIntermediateDirectories: true)
try fm.createFile(atPath: path, contents: data)
// Removal:
try fm.removeItem(at: url)
// Move and copy:
try fm.moveItem(at: source, to: dest)
try fm.copyItem(at: source, to: dest)
// Directory listing:
let contents = try fm.contentsOfDirectory(at: dirURL,
includingPropertiesForKeys: nil)
// Recursive:
let enumerator = fm.enumerator(at: dirURL,
includingPropertiesForKeys: [.fileSizeKey])
while let url = enumerator?.nextObject() as? URL {
print(url)
}
// Attributes:
let attrs = try fm.attributesOfItem(atPath: path)
let size = attrs[.size] as? Int
let date = attrs[.modificationDate] as? Date
URLs
Treated in Standard library.
import Foundation
let fileURL = URL(filePath: "/etc/hosts")
let webURL = URL(string: "https://example.com/path")!
fileURL.lastPathComponent // "hosts"
fileURL.pathExtension // ""
fileURL.deletingLastPathComponent()
fileURL.appendingPathComponent("more")
fileURL.appendingPathExtension("txt")
Common directories
import Foundation
let fm = FileManager.default
// Documents:
let docs = try fm.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
// Cache:
let cache = try fm.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
// Temp:
let temp = fm.temporaryDirectory
// Application Support:
let appSupport = try fm.url(for: .applicationSupportDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true)
// Home:
let home = fm.homeDirectoryForCurrentUser
HTTP via URLSession
Treated in Standard library.
import Foundation
let session = URLSession.shared
// Async (Swift 5.5+):
let (data, response) = try await session.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200..<300).contains(httpResponse.statusCode) else {
throw FetchError.badStatus
}
// JSON:
struct User: Decodable { let id: Int; let name: String }
let user = try JSONDecoder().decode(User.self, from: data)
// POST:
var request = URLRequest(url: apiURL)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(payload)
let (responseData, _) = try await session.data(for: request)
// Download to file:
let (location, _) = try await session.download(from: url)
try fm.moveItem(at: location, to: destination)
// Upload:
let (responseData, _) = try await session.upload(for: request, fromFile: fileURL)
// Streaming bytes:
let (bytes, _) = try await session.bytes(from: url)
for try await line in bytes.lines {
print(line)
}
Process I/O (subprocess)
import Foundation
let process = Process()
process.executableURL = URL(filePath: "/usr/bin/git")
process.arguments = ["status"]
let stdout = Pipe()
let stderr = Pipe()
process.standardOutput = stdout
process.standardError = stderr
try process.run()
process.waitUntilExit()
let outData = stdout.fileHandleForReading.readDataToEndOfFile()
let errData = stderr.fileHandleForReading.readDataToEndOfFile()
let output = String(data: outData, encoding: .utf8) ?? ""
let errors = String(data: errData, encoding: .utf8) ?? ""
if process.terminationStatus != 0 {
print("failed: \(errors)")
}
For substantial process management, the async forms via Pipe.fileHandleForReading.bytes admit streaming.
Pipes
let pipe = Pipe()
let writer = pipe.fileHandleForWriting
let reader = pipe.fileHandleForReading
try writer.write(contentsOf: "hello".data(using: .utf8)!)
try writer.close()
let data = reader.readDataToEndOfFile()
Common patterns
Read a file as string
let url = URL(filePath: "/path/to/file.txt")
let content = try String(contentsOf: url, encoding: .utf8)
Write a file atomically
let url = URL(filePath: "/path/to/file.txt")
try "content".write(to: url, atomically: true, encoding: .utf8)
The atomically: true admits substantial reliability — writes to a temp file then renames.
Read JSON from file
struct Config: Decodable {
let host: String
let port: Int
}
let url = Bundle.main.url(forResource: "config", withExtension: "json")!
let data = try Data(contentsOf: url)
let config = try JSONDecoder().decode(Config.self, from: data)
Stream lines from a file
let url = URL(filePath: "/path/to/large.log")
let handle = try FileHandle(forReadingFrom: url)
defer { try? handle.close() }
for try await line in handle.bytes.lines {
if line.contains("ERROR") {
print(line)
}
}
Walk a directory
let fm = FileManager.default
let baseURL = URL(filePath: ".")
if let enumerator = fm.enumerator(at: baseURL,
includingPropertiesForKeys: [.isRegularFileKey],
options: [.skipsHiddenFiles]) {
for case let url as URL in enumerator {
let isFile = (try? url.resourceValues(forKeys: [.isRegularFileKey]))?.isRegularFile ?? false
if isFile {
print(url.path)
}
}
}
Compute a file hash
import CryptoKit
let url = URL(filePath: path)
let data = try Data(contentsOf: url)
let hash = SHA256.hash(data: data)
let hexHash = hash.compactMap { String(format: "%02x", $0) }.joined()
For substantial files, streaming hash:
import CryptoKit
let handle = try FileHandle(forReadingFrom: url)
defer { try? handle.close() }
var hasher = SHA256()
while let chunk = try handle.read(upToCount: 64 * 1024), !chunk.isEmpty {
hasher.update(data: chunk)
}
let hash = hasher.finalize()
Check file metadata
let url = URL(filePath: path)
let attrs = try fm.attributesOfItem(atPath: url.path)
if let size = attrs[.size] as? Int { print("size: \(size)") }
if let date = attrs[.modificationDate] as? Date { print("modified: \(date)") }
if let perms = attrs[.posixPermissions] as? NSNumber {
print("perms: \(String(perms.intValue, radix: 8))")
}
HTTP fetch with progress
let (bytes, response) = try await URLSession.shared.bytes(from: url)
let total = response.expectedContentLength
var loaded: Int64 = 0
var data = Data()
for try await byte in bytes {
data.append(byte)
loaded += 1
if loaded % 100_000 == 0 {
print("\(loaded)/\(total) bytes")
}
}
Download to file
let (tempURL, _) = try await URLSession.shared.download(from: url)
let destination = fm.temporaryDirectory.appendingPathComponent("downloaded.dat")
try fm.moveItem(at: tempURL, to: destination)
Run a subprocess
import Foundation
func run(_ command: String, _ args: [String]) throws -> String {
let process = Process()
process.executableURL = URL(filePath: command)
process.arguments = args
let pipe = Pipe()
process.standardOutput = pipe
try process.run()
process.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
return String(data: data, encoding: .utf8) ?? ""
}
let output = try run("/usr/bin/git", ["status"])
print(output)
Atomic file write helper
extension Data {
func atomicWrite(to url: URL) throws {
let tempURL = url.deletingLastPathComponent().appendingPathComponent(UUID().uuidString)
try self.write(to: tempURL)
try FileManager.default.replaceItem(at: url, withItemAt: tempURL,
backupItemName: nil,
options: [],
resultingItemURL: nil)
}
}
The replaceItem admits atomic replacement on the same volume.
Concurrent file fetch
let urls = [url1, url2, url3]
let allData = try await withThrowingTaskGroup(of: Data.self) { group in
for url in urls {
group.addTask {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
}
var results: [Data] = []
for try await data in group {
results.append(data)
}
return results
}
Property list I/O
import Foundation
let dict: [String: Any] = ["name": "Alice", "age": 30, "items": [1, 2, 3]]
// Write:
let plistData = try PropertyListSerialization.data(
fromPropertyList: dict,
format: .xml,
options: 0
)
try plistData.write(to: plistURL)
// Read:
let read = try PropertyListSerialization.propertyList(
from: try Data(contentsOf: plistURL),
format: nil
) as? [String: Any]
For typed plists, PropertyListEncoder/PropertyListDecoder with Codable:
let data = try PropertyListEncoder().encode(user)
let decoded = try PropertyListDecoder().decode(User.self, from: data)
CSV reading
The standard library does not include CSV; conventional gems: swift-csv, CSVImporter. For ad-hoc:
let text = try String(contentsOf: url, encoding: .utf8)
let rows = text.split(separator: "\n").map { line in
line.split(separator: ",").map(String.init)
}
The form is rough; for substantial CSV (with quoted fields, escaped commas), use a library.
Reading from stdin
let stdin = FileHandle.standardInput
// Line-based:
while let line = readLine() {
process(line)
}
// Or async (with bytes):
for try await line in stdin.bytes.lines {
process(line)
}
A note on the conventional discipline
The contemporary Swift I/O advice:
- Use
URLfor file paths and remote addresses. - Use
String(contentsOf:)andData(contentsOf:)for whole-file reads. - Use
FileHandlefor streaming or substantial files. - Use
URLSession.data(from:)for async HTTP. - Use
bytes.linesfor async line-based reading. - Use
JSONEncoder/JSONDecoderwithCodablefor JSON. - Use
PropertyListEncoder/PropertyListDecoderfor plists. - Use
FileManagerfor file-system operations. - Use
Bundle.main/Bundle.modulefor resources. - Use
atomically: truewhen writing to admit substantial reliability. - Use
withTaskGroupfor concurrent I/O. - Use
ProcessandPipefor subprocesses.
The combination — Foundation’s substantial file API, URL-based addressing, async byte streams, the JSON/plist encoders, the substantial process I/O — is the substance of Swift’s I/O surface. The discipline produces concise, type-safe, async-friendly I/O code with substantial cross-platform support.