Polyglot
Languages Swift io
Swift § io

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 URL for file paths and remote addresses.
  • Use String(contentsOf:) and Data(contentsOf:) for whole-file reads.
  • Use FileHandle for streaming or substantial files.
  • Use URLSession.data(from:) for async HTTP.
  • Use bytes.lines for async line-based reading.
  • Use JSONEncoder/JSONDecoder with Codable for JSON.
  • Use PropertyListEncoder/PropertyListDecoder for plists.
  • Use FileManager for file-system operations.
  • Use Bundle.main/Bundle.module for resources.
  • Use atomically: true when writing to admit substantial reliability.
  • Use withTaskGroup for concurrent I/O.
  • Use Process and Pipe for 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.