Swift: Swift Input, Output, and Debugging
Writing code is only half the job. This lesson teaches you how to use print, assertions, and breakpoints to find and fix errors in your code.
1. What You'll Learn
- Advanced print parameters: separator and terminator
- The difference between debugPrint and dump and when to use each
- assert assertions and precondition checks
- String formatting and aligned output
- Playground live preview and breakpoint debugging basics
2. A True Story: A Self-Taught Beginner
(1) The Pain Point: Logically Correct Code, Wrong Results
Mike is a self-taught Swift beginner working on a program to calculate an average score:
let scores = [85, 92, 78, 90, 88]
var total = 0
for i in 0...5 {
total += scores[i]
}
let average = total / 5
print("Average: \(average)")
The program crashed immediately. Mike couldn't figure out why. He spent 30 minutes searching online, trying various fixes. Every modification required re-running, but he still couldn't pinpoint the problem.
(2) The Solution: Print-Based Debugging
A friend suggested using print to output intermediate variables to locate the issue:
let scores = [85, 92, 78, 90, 88]
print("Array length: \(scores.count)") // Output: 5
var total = 0
for i in 0..<scores.count { // Use count instead of hardcoding
print("Processing index \(i): value = \(scores[i])")
total += scores[i]
}
print("Total: \(total)")
let average = total / 5
print("Average: \(average)")
Output:
TEXT 📖 Display onlyArray length: 5 Processing index 0: value = 85 Processing index 1: value = 92 Processing index 2: value = 78 Processing index 3: value = 90 Processing index 4: value = 88 Total: 433 Average: 86
By printing intermediate variables, Mike immediately discovered that the array indices are 0-4, but he had used 0...5 (which includes index 5).
(3) The Result: Debugging Efficiency After Mastering Print
| Dimension | Before (Guessing) | After (Print Debugging) |
|---|---|---|
| Time to locate issue | 30+ minutes | 1-2 minutes |
| Fix accuracy | 50% (often wrong) | 95% |
| Code understanding | "No idea why it crashed" | "Know the state at every step" |
| Confidence solving new issues | 3/10 | 8/10 |
3. Advanced Print Usage
print is Swift's most commonly used debugging tool, but its parameters do more than just output a value.
graph TB
A[print Function] --> B["items: Values to output"]
A --> C["separator: Delimiter"]
A --> D["terminator: End character"]
A --> E["to: Output target"]
B --> F[Multiple values separated by commas]
C --> G["Default: space"]
C --> H["Custom: | or , etc."]
D --> I["Default: newline"]
D --> J["Custom: empty string"]
| Parameter | Type | Default | Purpose |
|---|---|---|---|
items |
Any... |
Required | Content to output |
separator |
String |
" " |
Separator between multiple items |
terminator |
String |
"\n" |
Trailing newline character |
to |
TextOutputStream |
nil |
Output target (default: console) |
(1) separator and terminator
// Default: space-separated, newline-terminated
print("Hello", "Swift", "World")
// Hello Swift World
// Custom separator
print("Hello", "Swift", "World", separator: ", ")
// Hello, Swift, World
// Custom terminator (no newline)
print("Loading", terminator: "...")
print("Done")
// Loading...Done
// Combined usage
print("A", "B", "C", separator: " | ", terminator: ".\n")
// A | B | C.
(2) debugPrint and dump
debugPrint outputs debug information (with quotes and type info); dump outputs detailed structures:
let name = "Alice"
let numbers = [1, 2, 3]
print(name) // Alice
debugPrint(name) // "Alice"
dump(name) // - "Alice"
print(numbers) // [1, 2, 3]
debugPrint(numbers) // [1, 2, 3]
dump(numbers)
// ▿ 3 elements
// - 0 : 1
// - 1 : 2
// - 2 : 3
| Function | Purpose | String Output | Array Output |
|---|---|---|---|
print |
Normal output | Alice | [1, 2, 3] |
debugPrint |
Debug output (shows type info) | "Alice" | [1, 2, 3] |
dump |
Detailed structure output | - "Alice" | One element per line |
▶ Example: Formatted Log Output
// ============================================
// Simulate system log output
// Demonstrates advanced print parameters and debugPrint
// ============================================
let event = "USER_LOGIN"
let user = "Alice"
let statusCode = 200
let duration = 0.045
// 1. Use separator to format the log
print("[\(event)]", user, "Status: \(statusCode)", separator: " | ", terminator: "")
print(" (\(duration)s)")
// [USER_LOGIN] | Alice | Status: 200 (0.045s)
// 2. Output tabular data
print()
print("=== Report ===")
print("Item", "Price", "Qty", separator: " | ")
print("-----", "-----", "---", separator: " | ")
print("Book", "12.99", "3", separator: " | ")
print("Pen", "1.50", "10", separator: " | ")
print("Bag", "49.99", "1", separator: " | ")
// 3. debugPrint for development debugging
let input: String? = "test"
debugPrint("Debug: input = \(input)")
// "Debug: input = Optional(\"test\")"
Output:
TEXT 📖 Display only[USER_LOGIN] | Alice | Status: 200 (0.045s) === Report === Item | Price | Qty ----- | ----- | --- Book | 12.99 | 3 Pen | 1.50 | 10 Bag | 49.99 | 1 Debug: input = Optional("test")
4. Assertions and Preconditions
Assertions and preconditions are Swift's built-in defensive programming tools, catching logic errors early during development.
graph LR
A[Runtime Checks] --> B[assert]
A --> C[precondition]
B --> D[Debug-only mode]
B --> E[Catch issues during development]
C --> F[Debug + Release]
C --> G[Unrecoverable errors]
| Function | Active In | Purpose | Example |
|---|---|---|---|
assert |
Debug only | Internal consistency checks during development | assert(age > 0) |
assertionFailure |
Debug only | Unconditional assertion trigger | assertionFailure("Should not reach here") |
precondition |
All modes | Precondition checks | precondition(!name.isEmpty) |
preconditionFailure |
All modes | Unconditional termination | preconditionFailure("Fatal error") |
(1) assert Debug Assertions
assert only takes effect in Debug mode; it is removed in Release mode, so there is no performance impact:
func calculateDiscount(price: Double, percent: Double) -> Double {
assert(price > 0, "Price must be greater than 0")
assert(percent >= 0 && percent <= 100, "Discount must be between 0-100")
let discount = price * percent / 100.0
return price - discount
}
let finalPrice = calculateDiscount(price: 100.0, percent: 20)
print(finalPrice) // 80.0
// The following would trigger assertion failures in Debug mode:
// calculateDiscount(price: -10, percent: 20) // ❌ assert fails
// calculateDiscount(price: 100, percent: 150) // ❌ assert fails
(2) precondition Checks
precondition takes effect in both Debug and Release modes—it terminates the program immediately when a condition cannot be satisfied:
func sendEmail(to address: String, message: String) {
precondition(address.contains("@"), "Invalid email address: \(address)")
precondition(!message.isEmpty, "Message cannot be empty")
print("Sending email to \(address): \(message)")
}
sendEmail(to: "alice@example.com", message: "Hello!")
// Sending email to alice@example.com: Hello!
// The following would trigger precondition failures (all modes):
// sendEmail(to: "invalid", message: "Hi")
▶ Example: Parameter Validation
// ============================================
// User registration parameter checks
// Demonstrates assert and precondition usage
// ============================================
import Foundation
func registerUser(name: String, age: Int, email: String) {
// precondition: Public API contract (active in all modes)
precondition(name.count >= 2, "Username must be at least 2 characters")
precondition(age >= 18, "User must be at least 18 years old")
precondition(email.contains("@"), "Invalid email format")
// assert: Internal logic check (Debug only)
let emailParts = email.split(separator: "@")
assert(emailParts.count == 2, "Email should contain exactly one @ symbol")
let domain = String(emailParts[1])
assert(domain.contains("."), "Email domain is invalid")
// Actual registration logic
print("Registration successful: \(name), age \(age)")
print("Confirmation email sent to: \(email)")
}
// Valid call
registerUser(name: "Alice", age: 28, email: "alice@example.com")
print("---")
// Calls that would trigger precondition failures (commented out to avoid crashing)
// registerUser(name: "A", age: 20, email: "test@test.com")
Output:
TEXT 📖 Display onlyRegistration successful: Alice, age 28 Confirmation email sent to: alice@example.com ---
5. Playground Debugging
Playgrounds offer more powerful debugging capabilities than print, including live previews and breakpoints.
(1) Playground Live Preview
The sidebar in Playgrounds shows the result of each line in real time:
// Run in Playground — the sidebar shows each step's result
let name = "Alice" // "Alice"
var score = 0 // 0
score += 85 // 85
score += 92 // 177
let average = score / 2 // 88
graph TB
A[Playground Debugging] --> B[Live Results Panel]
A --> C[Breakpoint Debugging]
A --> D[Value History]
B --> E[Results displayed per line]
C --> F[Pause / Step / Continue]
D --> G[Variable value change curves]
| Feature | How To Use | Purpose |
|---|---|---|
| Live preview | Code editing auto-displays results on the right | Quickly see each step's result |
| Breakpoints | Click the line number to add a breakpoint | Pause execution, trace step by step |
| Value history | Hover over a variable | See how a variable changes over time |
| Expression preview | Select code | Quickly evaluate a selected expression |
(2) Breakpoint Debugging
Click on the left side of a line number to set a breakpoint. When the program reaches that line, it pauses so you can inspect all current variable values:
func calculateTotal(items: [Double], tax: Double) -> Double {
var subtotal = 0.0
// Set a breakpoint on this line
for item in items {
subtotal += item
}
let taxAmount = subtotal * tax
let total = subtotal + taxAmount
return total
}
let cart = [29.99, 49.99, 15.00]
let final = calculateTotal(items: cart, tax: 0.08)
print("Total: $\(final)")
▶ Example: Playground Debugging in Practice
// ============================================
// Practice Playground debugging techniques
// Paste this code into a Playground and run it
// ============================================
import Foundation
// 1. Set a breakpoint on the line below to observe variable values
let data: [String: Any] = [
"product": "Swift Book",
"price": 39.99,
"quantity": 3,
"inStock": true
]
// 2. Step through the following code
let productName = data["product"] as? String ?? "Unknown"
let price = data["price"] as? Double ?? 0.0
let quantity = data["quantity"] as? Int ?? 0
let inStock = data["inStock"] as? Bool ?? false
print("Product: \(productName)")
print("Price: $\(price)")
print("Quantity: \(quantity)")
print("In Stock: \(inStock)")
// 3. Observe the conditional check results
if inStock {
let totalCost = price * Double(quantity)
print("Total Cost: $\(totalCost)")
} else {
print("Item is out of stock")
}
// 4. Use dump to inspect complex data
print("\n=== Debug Info ===")
dump(data)
Output:
TEXT 📖 Display onlyProduct: Swift Book Price: $39.99 Quantity: 3 In Stock: true Total Cost: $119.97 === Debug Info === ▿ 4 key/value pairs ▿ (2 elements) - key: "product" - value: "Swift Book" ▿ (2 elements) - key: "price" - value: 39.99 ▿ (2 elements) - key: "quantity" - value: 3 ▿ (2 elements) - key: "inStock" - value: true
6. Full Example: Grade Analysis Debugging Tool
// ============================================
// Grade analysis tool
// Combines print debugging + assertion checks + formatted output
// ============================================
import Foundation
// 1. Student grade data
let studentName = "Alice"
let scores = [85.0, 92.0, 78.0, 90.0, 88.0]
// 2. Debug assertions to validate data
assert(scores.count > 0, "Score list cannot be empty")
for score in scores {
assert(score >= 0 && score <= 100, "Score must be between 0-100: \(score)")
}
// 3. Calculate statistics
var total = 0.0
var highest = scores[0]
var lowest = scores[0]
// Set a breakpoint here to observe the loop process
for (index, score) in scores.enumerated() {
print("[DEBUG] Index \(index): \(score)")
total += score
if score > highest { highest = score }
if score < lowest { lowest = score }
}
let average = total / Double(scores.count)
// 4. Formatted output
print(String(repeating: "=", count: 35))
print("Student: \(studentName)")
print(String(repeating: "-", count: 35))
print("Subject", "Score", "Grade", separator: " | ")
print(String(repeating: "-", count: 35))
for (index, score) in scores.enumerated() {
let grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "D"
print("Subject \(index + 1)", score, grade, separator: " | ")
}
print(String(repeating: "-", count: 35))
print("Total: \(total)")
print("Average: \(String(format: "%.1f", average))")
print("Highest: \(highest)")
print("Lowest: \(lowest)")
print(String(repeating: "=", count: 35))
// 5. Precondition to ensure reasonable results
precondition(average >= 0 && average <= 100, "Average is outside expected range")
precondition(highest >= lowest, "Highest score should not be lower than lowest")
Output:
TEXT 📖 Display only[DEBUG] Index 0: 85.0 [DEBUG] Index 1: 92.0 [DEBUG] Index 2: 78.0 [DEBUG] Index 3: 90.0 [DEBUG] Index 4: 88.0 =================================== Student: Alice ----------------------------------- Subject | Score | Grade ----------------------------------- Subject 1 | 85.0 | B Subject 2 | 92.0 | A Subject 3 | 78.0 | C Subject 4 | 90.0 | A Subject 5 | 88.0 | B ----------------------------------- Total: 433.0 Average: 86.6 Highest: 92.0 Lowest: 78.0 ===================================
❓ FAQ
print outputs in a human-readable format (e.g. Alice), while debugPrint outputs in a debugging-oriented format (e.g. "Alice" with quotes). Custom types can implement CustomStringConvertible and CustomDebugStringConvertible protocols to control both outputs.assert only takes effect in Debug mode (-Onone). In Release mode, the compiler skips evaluation and execution of asserts. Therefore, never put side-effect logic inside an assert.precondition takes effect in all modes and is for unrecoverable fatal errors. Examples: pre-array-bounds check, required parameter being nil, a branch that should logically never be reached. Use precondition for public API parameter validation.📖 Summary
print'sseparatorcustomizes the delimiter between items;terminatorcontrols the end characterdebugPrintshows output with type information;dumpshows detailed structuresassertchecks internal logic consistency in Debug modepreconditionchecks unrecoverable preconditions in all modes- The Playground sidebar displays each line's result in real time
- Breakpoint debugging lets you step through code line by line and observe variable changes
📝 Exercises
- Beginner: Use
printto generate a simple multiplication table (1-3), controlling the format withseparatorandterminatorto output a table style. - Intermediate: Write a function
divide(_ a: Double, by b: Double) -> Double, usingpreconditionto check the divisor is not 0 andassertto verify the result is within a reasonable range. Then useprintwith formatting to output the result. - Challenge: Simulate an ATM withdrawal program. Use
assertto verify the withdrawal amount is a multiple of 100, andpreconditionto check sufficient balance. Useprintwithseparatorandterminatorto format transaction details including time, amount, and balance.