Intermediate

I/O Operations in Swift

Learn console input and output, file reading and writing, and I/O error handling in Swift with Docker-ready examples

Input and output are where a program meets the outside world, and Swift approaches them with the same philosophy it applies everywhere else: safety first. Reading from the console returns an optional, because input might not exist. File operations throw errors, because disks fail and paths go missing. Instead of sentinel values or silent crashes, Swift’s type system forces you to acknowledge these possibilities at compile time.

Swift’s I/O story spans two layers. The language itself provides print() and readLine() for console work — no imports required. Everything beyond that lives in Foundation, the core library that supplies FileManager, FileHandle, and the String and Data conveniences for reading and writing files. Foundation ships with Swift on every platform, including Linux, so the same code runs on macOS, in Docker, and on a server.

In this tutorial you’ll format console output with separators, terminators, and String(format:); read and parse input from standard input; and write, read, and append to files with proper do/try/catch error handling. Along the way you’ll see how optionals and thrown errors — two of Swift’s signature features — shape its I/O APIs.

Console Output Beyond the Basics

print() accepts multiple values plus separator and terminator parameters, and String(format:) brings printf-style formatting when you need precise control.

Create a file named io_console.swift:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import Foundation

// Basic output — print adds a newline by default
print("Standard output with a newline")

// Custom terminator suppresses the newline
print("No newline here", terminator: "")
print(" -- continued on the same line")

// Multiple values with a custom separator
print("Swift", "is", "expressive", separator: " | ")
print(1, 2, 3, separator: ", ")

// String interpolation handles any type
let language = "Swift"
let version = 6.0
print("\(language) version \(version)")

// printf-style formatting via Foundation
let pi = 3.14159265
print(String(format: "Pi to two decimals: %.2f", pi))
print(String(format: "Padded integer: %5d", 42))

The separator and terminator parameters have default values (" " and "\n"), so you only specify them when you want different behavior — a small example of Swift’s default-parameter design keeping the common case clean.

Reading from Standard Input

readLine() reads one line from standard input and returns String? — an optional, because the stream may be exhausted. Unwrapping that optional with if let is the idiomatic way to handle input safely.

Create a file named io_input.swift:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// readLine() returns String? — nil when input runs out
print("What is your name?")
if let name = readLine() {
    print("Hello, \(name)!")
} else {
    print("No input received.")
}

// Parsing numbers from a line of input
print("Enter two numbers separated by a space:")
if let line = readLine() {
    let numbers = line.split(separator: " ").compactMap { Int($0) }
    if numbers.count == 2 {
        print("Sum: \(numbers[0] + numbers[1])")
    } else {
        print("Please enter exactly two integers.")
    }
}

Notice the functional style in the parsing step: split(separator:) breaks the line into pieces, and compactMap converts each piece to Int while discarding any that fail — Int("abc") returns nil, and compactMap drops the nils. No manual loop, no exception handling, just a pipeline of transformations.

File I/O with Error Handling

File operations in Swift throw errors rather than returning status codes. You mark each risky call with try and wrap the sequence in do/catch — the compiler refuses to let you ignore a throwing call, so forgotten error checks simply can’t happen.

Create a file named io_files.swift:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import Foundation

let path = "journal.txt"

// Writing: String.write(toFile:) creates or overwrites the file
let entry = """
Day 1: Started learning Swift I/O.
Day 2: Wrote my first file from Swift.
"""

do {
    try entry.write(toFile: path, atomically: true, encoding: .utf8)
    print("Wrote \(path)")
} catch {
    print("Write failed: \(error)")
}

// Reading: String(contentsOfFile:) loads the whole file
do {
    let contents = try String(contentsOfFile: path, encoding: .utf8)
    print("File contents:")
    print(contents)
} catch {
    print("Read failed: \(error)")
}

// Appending: FileHandle gives lower-level, positional access
do {
    let handle = try FileHandle(forWritingTo: URL(fileURLWithPath: path))
    try handle.seekToEnd()
    try handle.write(contentsOf: Data("\nDay 3: Appended a line with FileHandle.".utf8))
    try handle.close()
    print("Appended a third entry")
} catch {
    print("Append failed: \(error)")
}

// FileManager answers questions about the filesystem
if FileManager.default.fileExists(atPath: path) {
    print("\(path) exists")
}

// Verify the final result
do {
    let final = try String(contentsOfFile: path, encoding: .utf8)
    print("Final contents:")
    print(final)
} catch {
    print("Final read failed: \(error)")
}

The atomically: true argument tells Swift to write to a temporary file first and rename it into place, so a crash mid-write can never leave a half-written file behind. FileHandle is the lower-level tool — it works with raw Data and byte positions, which is why appending uses it instead of the string conveniences.

Running with Docker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Pull the official Swift image
docker pull swift:6.0

# Run the console output example
docker run --rm -v $(pwd):/app -w /app swift:6.0 swift io_console.swift

# Run the input example, piping in two lines of stdin (-i keeps stdin open)
printf 'Ada\n7 35\n' | docker run -i --rm -v $(pwd):/app -w /app swift:6.0 swift io_input.swift

# Run the file I/O example
docker run --rm -v $(pwd):/app -w /app swift:6.0 swift io_files.swift

Because the current directory is mounted at /app, the journal.txt created by the file example appears in your working directory on the host — inspect it after the run.

Expected Output

Running io_console.swift:

Standard output with a newline
No newline here -- continued on the same line
Swift | is | expressive
1, 2, 3
Swift version 6.0
Pi to two decimals: 3.14
Padded integer:    42

Running io_input.swift with the piped input shown above:

What is your name?
Hello, Ada!
Enter two numbers separated by a space:
Sum: 42

Running io_files.swift:

Wrote journal.txt
File contents:
Day 1: Started learning Swift I/O.
Day 2: Wrote my first file from Swift.
Appended a third entry
journal.txt exists
Final contents:
Day 1: Started learning Swift I/O.
Day 2: Wrote my first file from Swift.
Day 3: Appended a line with FileHandle.

Key Concepts

  • readLine() returns an optionalString? forces you to handle end-of-input explicitly, applying Swift’s nil safety to the unpredictable world of user input
  • File operations throwtry marks every call that can fail, and the compiler requires a do/catch (or explicit propagation), so I/O errors can’t be silently ignored
  • print() is configurableseparator and terminator parameters with sensible defaults handle multi-value and same-line output without a formatting mini-language
  • Foundation is the I/O toolkitString(format:), FileManager, FileHandle, and the file-reading String initializers all require import Foundation, on Linux as well as macOS
  • compactMap shines at input parsing — converting strings to numbers while discarding failures is a one-line functional pipeline instead of a validation loop
  • Atomic writes prevent corruptionatomically: true writes to a temporary file and renames it, so readers never observe a partially written file
  • Two levels of file APIString/Data conveniences read or write whole files in one call; FileHandle offers positional access (seek, append, partial reads) when you need it
  • Standard error is available tooFileHandle.standardError.write(_:) sends diagnostics to stderr, keeping them separate from program output on stdout

Running Today

All examples can be run using Docker:

docker pull swift:6.0
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining