Ruby.CodeCompared.To/Swift

An interactive executable cheatsheet for Rubyists learning Swift

Ruby 4.0 Swift 6.3
Syntax Basics
Variables & constants
x = 10 # mutable by default name = "Alice" GRAVITY = 9.8 # uppercase = constant convention x = 20 # reassignment fine puts x puts name puts GRAVITY
var x = 10 // mutable let name = "Alice" // immutable constant let gravity = 9.8 // constants use let, not uppercase x = 20 // OK β€” var is mutable // name = "Bob" // compile error β€” let is immutable print(x) print(name) print(gravity)

Swift's let creates a truly immutable binding β€” the compiler enforces it, unlike Ruby's uppercase convention. Use let by default and reach for var only when mutation is needed. The compiler will warn you if a var is never mutated.

String interpolation
name = "World" count = 42 puts "Hello, #{name}!" puts "Count is #{count * 2}" puts "Sum: #{(1..10).sum}"
let name = "World" let count = 42 print("Hello, \(name)!") print("Count is \(count * 2)") print("Sum: \((1...10).reduce(0, +))")

Ruby uses #{ } for interpolation; Swift uses \( ). Both evaluate arbitrary expressions inside. Swift 5.7+ allows multiline expressions inside interpolations.

Comments
# Single-line comment x = 42 # inline comment =begin Multi-line comment block (rarely used in practice) =end puts x
// Single-line comment let x = 42 // inline comment /* Multi-line comment β€” these can be /* nested */ which Ruby's cannot */ print(x)

Swift uses C-style // and /* */ comments. Swift's block comments support nesting (/* /* inner */ outer */), which is useful for commenting out regions that already contain comments β€” impossible in Ruby's =begin/=end.

Output / printing
puts "With newline" # adds \n print "No newline"; print "\n" p [1, 2, 3] # inspect: [1, 2, 3] puts [1, 2, 3] # one element per line
print("With newline") print("No newline", terminator: ""); print("") print([1, 2, 3]) // [1, 2, 3] dump([1, 2, 3]) // detailed debug output

Swift's print() adds a newline by default. Use the terminator: parameter to suppress it. Swift has no direct equivalent to Ruby's p, but dump() gives detailed recursive output for debugging.

Types & Data
Type inference & annotations
number = 42 # Integer price = 9.99 # Float greeting = "Hi" # String active = true # TrueClass puts number.class puts price.class puts greeting.class
let number = 42 // inferred: Int let price = 9.99 // inferred: Double let greeting = "Hi" // inferred: String let active = true // inferred: Bool // Explicit annotations: let count: Int = 0 let ratio: Float = 0.5 print(type(of: number), type(of: price))

Swift infers types at compile time and they are fixed β€” a variable cannot change type after declaration. Ruby's type system is dynamic: any variable can hold any type. Swift's type(of:) is analogous to Ruby's .class.

Booleans & truthiness
puts true && false # false puts true || false # true puts !true # false # Only false and nil are falsy in Ruby: puts "0 is truthy" if 0 puts "empty str truthy" if "" puts "empty arr truthy" if []
print(true && false) // false print(true || false) // true print(!true) // false // Swift requires strict Bool β€” no truthiness coercion: if 1 == 1 { print("must be explicit Bool") } // if 0 { } // compile error: integer is not a Bool // if "" { } // compile error: string is not a Bool

In Ruby, only false and nil are falsy β€” 0, "", and [] are all truthy. Swift requires a strict Bool in every condition. This prevents a whole class of subtle bugs where non-nil values are used as conditionals unintentionally.

Optionals
Declaring & checking optionals
# Every Ruby variable can be nil name = nil name = "Alice" puts name.nil? # false puts name&.upcase # safe navigation: ALICE missing = nil puts missing&.upcase.inspect # nil β€” no crash
var name: String? = nil // optional String name = "Alice" print(name == nil) // false print(name?.uppercased()) // Optional("ALICE") let missing: String? = nil print(missing?.uppercased() as Any) // nil β€” no crash // var required: String = nil // compile error!

Swift's type system distinguishes String (never nil) from String? (may be nil). Ruby's safe navigation &. maps directly to Swift's optional chaining ?.. The key improvement: in Swift, a non-optional type cannot be nil β€” the compiler guarantees it.

Unwrapping: if let
input = "42" number = Integer(input) rescue nil if number puts "Got: #{number * 2}" else puts "Not a number" end
let input = "42" if let number = Int(input) { // unwrap and bind print("Got: \(number * 2)") // number is plain Int here } else { print("Not a number") } // Swift 5.7+ shorthand (same name): if let number = Int("99") { print(number) }

if let simultaneously checks for nil and unwraps the value into a non-optional binding. Inside the block, number is a plain Int, not Int?. Swift 5.7 added the shorthand if let x { } when shadowing the same name.

Unwrapping: guard let
def process(input) number = Integer(input) rescue nil return "invalid" unless number # number is guaranteed non-nil from here number * 2 end puts process("21") puts process("abc")
func process(_ input: String) -> String { guard let number = Int(input) else { return "invalid" } // number is a plain Int in the enclosing scope return "\(number * 2)" } print(process("21")) print(process("abc"))

guard let is Swift's return unless. Unlike if let, the unwrapped binding is available in the enclosing scope after the guard, not just inside a block. The else branch must exit (return, throw, or break) β€” the compiler enforces this.

Nil coalescing
name = nil display = name || "Anonymous" puts display flag = false result = flag || "default" # replaces false too! puts result
let name: String? = nil let display = name ?? "Anonymous" print(display) // ?? only replaces nil, not false: let flag: Bool? = false let result = flag ?? true // result is false! print(result)

Swift's ?? only replaces nil β€” it does not treat false, 0, or "" as absent. Ruby's || replaces both nil and false. This distinction matters when working with optional booleans.

Strings
Common string methods
greeting = "hello world" puts greeting.upcase puts greeting.length puts greeting.include?("world") puts greeting.split(" ").inspect puts greeting.gsub("world", "Swift")
import Foundation let greeting = "hello world" print(greeting.uppercased()) print(greeting.count) print(greeting.contains("world")) print(greeting.components(separatedBy: " ")) print(greeting.replacingOccurrences(of: "world", with: "Swift"))

Swift string methods are more verbose: uppercased() not upcase, count not length. Methods from Foundation (like components(separatedBy:) and replacingOccurrences) require import Foundation, which is included automatically on Apple platforms.

Multiline strings
text = <<~HEREDOC Line one Line two Indentation stripped to match marker HEREDOC puts text
let text = """ Line one Line two Indentation stripped to match closing delimiter """ print(text)

Both languages strip leading indentation from multiline strings. Ruby uses squiggly heredoc (<<~HEREDOC); Swift uses triple-quoted strings ("""). Swift requires the closing """ on its own line; the amount of indentation it has sets the strip level.

Collections
Arrays
numbers = [1, 2, 3, 4, 5] numbers.push(6) # or numbers << 6 numbers.pop puts numbers.first puts numbers.last puts numbers.length puts numbers.include?(3) puts numbers.inspect
var numbers = [1, 2, 3, 4, 5] numbers.append(6) numbers.removeLast() print(numbers.first!) // first returns Int? β€” unwrap safely print(numbers.last!) print(numbers.count) print(numbers.contains(3)) print(numbers)

Swift arrays are typed β€” [Int], [String], etc. β€” and cannot mix types without [Any]. first and last return optionals (Int?) since the array might be empty. The ! force-unwrap here is safe only because we know the array is non-empty.

Dictionaries
person = {name: "Alice", age: 30} puts person[:name] puts person[:age] person[:email] = "alice@example.com" puts person.keys.inspect
var person = ["name": "Alice", "age": "30"] print(person["name"] ?? "unknown") // subscript returns String? print(person["age"] ?? "unknown") person["email"] = "alice@example.com" print(Array(person.keys).sorted())

Dictionary subscript in Swift always returns an optional (Value?) since the key might not exist. In Ruby, a missing key silently returns nil. Swift makes the "might be absent" case explicit β€” you must handle it with ??, if let, or guard let.

map / filter / reduce
numbers = [1, 2, 3, 4, 5] doubled = numbers.map { |x| x * 2 } evens = numbers.select { |x| x.even? } # filter total = numbers.reduce(0) { |sum, x| sum + x } puts doubled.inspect puts evens.inspect puts total
let numbers = [1, 2, 3, 4, 5] let doubled = numbers.map { $0 * 2 } let evens = numbers.filter { $0 % 2 == 0 } let total = numbers.reduce(0) { $0 + $1 } // or: numbers.reduce(0, +) print(doubled) print(evens) print(total)

Swift's map, filter, and reduce mirror Ruby's almost exactly. Swift uses $0, $1 as shorthand argument names (like Ruby's |x|). Note: Ruby calls it select; Swift calls it filter. Swift can use operators like + directly as closures.

Control Flow
if / else if / else
score = 85 if score >= 90 puts "A" elsif score >= 80 puts "B" else puts "C or below" end puts "pass" if score >= 60 # postfix if
let score = 85 if score >= 90 { print("A") } else if score >= 80 { // "else if", not "elsif" print("B") } else { print("C or below") } print(score >= 60 ? "pass" : "fail") // ternary instead

Swift uses else if (two words) where Ruby uses elsif. Braces { } are always required β€” no then or end. Swift has no postfix if; use the ternary operator for one-liners.

switch / case
day = "Monday" case day when "Saturday", "Sunday" puts "Weekend" when /^Mon|Tue|Wed|Thu|Fri/ puts "Weekday" else puts "Unknown" end
let day = "Monday" switch day { case "Saturday", "Sunday": print("Weekend") case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday": print("Weekday") default: print("Unknown") } // must be exhaustive β€” no silent fall-through

Swift switch must be exhaustive and does not fall through by default (unlike C). Multiple values per case use commas. Swift's switch is far more powerful than Ruby's β€” it supports tuples, ranges, value bindings, and where clauses.

Loops
(1..5).each { |i| print "#{i} " }; puts [10,20,30].each_with_index do |val, idx| print "#{idx}:#{val} " end; puts 3.times { |i| print "#{i} " }
for i in 1...5 { print("\(i) ") }; print("") for (idx, val) in [10,20,30].enumerated() { print("\(idx):\(val) ", terminator: "") }; print("") for i in 0..<3 { print("\(i) ", terminator: "") }

⚠ Range operators are reversed compared to Ruby: Swift's 1...5 is inclusive (like Ruby's 1..5), and 1..<5 is exclusive (like Ruby's 1...5). Swift's enumerated() replaces Ruby's each_with_index.

Methods & Functions
Function definition
def greet(name) "Hello, #{name}!" # implicit return end def add(a, b) = a + b # one-liner (Ruby 4.0) puts greet("Alice") puts add(3, 4)
func greet(name: String) -> String { "Hello, \(name)!" // single expr: implicit return } func add(_ a: Int, _ b: Int) -> Int { a + b } print(greet(name: "Alice")) print(add(3, 4))

Swift functions require type annotations and an explicit -> ReturnType. Single-expression bodies return implicitly (Swift 5.1+). The _ before a parameter drops its external label, allowing add(3, 4) instead of the default add(a: 3, b: 4).

Labeled / keyword parameters
def connect(host:, port: 80, ssl: false) "#{ssl ? 'https' : 'http'}://#{host}:#{port}" end puts connect(host: "example.com") puts connect(host: "api.io", port: 443, ssl: true)
func connect(host: String, port: Int = 80, ssl: Bool = false) -> String { "\(ssl ? "https" : "http")://\(host):\(port)" } print(connect(host: "example.com")) print(connect(host: "api.io", port: 443, ssl: true))

Swift requires argument labels at every call site by default β€” callers must write connect(host: ...). This is the opposite of Ruby, where positional arguments are the default. Both languages support default parameter values. Swift's labels make call sites read like natural English.

Closures
Closure syntax
double = ->(x) { x * 2 } puts double.call(5) words = ["hello", "world"] puts words.map(&:upcase).inspect puts words.map { |w| w.upcase }.inspect
let double = { (x: Int) -> Int in x * 2 } print(double(5)) let words = ["hello", "world"] print(words.map { $0.uppercased() }) // $0 = first arg print(words.map { w in w.uppercased() }) // named arg

Swift closures use { args in body } syntax. $0, $1… are shorthand argument names equivalent to Ruby's |x|, |y|. Swift can pass operators directly where a compatible closure is expected: .sorted(by: <) or .reduce(0, +).

Trailing closures
def repeat_work(times) times.times { yield } end repeat_work(3) { print "hello " } puts
func repeatWork(_ times: Int, work: () -> Void) { for _ in 1...times { work() } } repeatWork(3) { print("hello ", terminator: "") } print("")

Swift's trailing closure syntax (closure as the last argument, placed outside the parentheses) is directly analogous to Ruby's block syntax. If a function has only a closure argument, the parentheses can be dropped entirely: repeatWork(3) { ... } β€” just like Ruby blocks.

Classes & Structs
Class definition
class Animal attr_reader :name, :sound def initialize(name, sound) @name = name @sound = sound end def speak = "#{@name} says #{@sound}!" end dog = Animal.new("Rex", "Woof") puts dog.speak puts dog.name
class Animal { let name: String let sound: String init(name: String, sound: String) { self.name = name self.sound = sound } func speak() -> String { "\(name) says \(sound)!" } } let dog = Animal(name: "Rex", sound: "Woof") print(dog.speak()) print(dog.name)

Swift stores properties as typed members β€” no @instance_var convention. init replaces initialize. Properties declared with let are read-only (like attr_reader); var is read-write (like attr_accessor). No end keyword β€” just closing braces.

Inheritance
class Animal attr_reader :name, :sound def initialize(name, sound) @name = name @sound = sound end def speak = "#{@name} says #{@sound}!" end class Dog < Animal def initialize(name) super(name, "Woof") end def fetch(item) = "#{@name} fetches #{item}!" end dog = Dog.new("Buddy") puts dog.speak # inherited puts dog.fetch("ball")
class Animal { let name: String let sound: String init(name: String, sound: String) { self.name = name self.sound = sound } func speak() -> String { "\(name) says \(sound)!" } } class Dog: Animal { // colon, not < init(name: String) { super.init(name: name, sound: "Woof") } func fetch(_ item: String) -> String { "\(name) fetches \(item)!" } } let dog = Dog(name: "Buddy") print(dog.speak()) // inherited print(dog.fetch("ball"))

Swift uses class Dog: Animal (colon) where Ruby uses class Dog < Animal (angle bracket). super.init() must be called before accessing self. Overriding a method requires the override keyword β€” the compiler rejects accidental overrides without it.

Structs β€” value types
# Ruby Struct is reference-based Point = Struct.new(:x, :y) a = Point.new(1, 2) b = a # same object! b.x = 99 puts a.x # 99 β€” shared reference
struct Point { var x: Int var y: Int } var a = Point(x: 1, y: 2) var b = a // COPY β€” value type b.x = 99 print(a.x) // 1 β€” independent copy print(b.x) // 99

Swift structs are value types β€” assignment copies the data. Classes are reference types β€” assignment shares the reference. Most Swift standard library types (Array, Dictionary, String) are structs. Prefer structs for plain data; use classes when shared identity or inheritance is needed.

Protocols
Protocol definition & conformance
module Describable def describe "I am a #{self.class} named #{name}" end end class Person include Describable attr_reader :name def initialize(name) = @name = name end puts Person.new("Alice").describe
protocol Describable { var name: String { get } // required property } extension Describable { func describe() -> String { // default implementation "I am a \(type(of: self)) named \(name)" } } struct Person: Describable { let name: String } print(Person(name: "Alice").describe())

Swift protocols declare a contract (required properties and methods); protocol extensions provide default implementations β€” analogous to Ruby modules with methods. Protocols work with both classes and structs, while Ruby mixins work only with classes. A type can conform to multiple protocols.

Enums
Basic enum
module Direction NORTH = :north SOUTH = :south EAST = :east WEST = :west end direction = Direction::NORTH puts direction puts direction == :north
enum Direction { case north, south, east, west } let direction = Direction.north print(direction) print(direction == .north) switch direction { case .north: print("Going north") default: print("Other direction") }

Swift enums are first-class types, not symbol conventions. The compiler enforces exhaustive switching over enum cases β€” if you add a new case, every switch that doesn't have a default breaks at compile time, forcing you to handle the new case everywhere.

Enums with associated values
# Ruby: simulate with tagged union Result = Struct.new(:type, :value) success = Result.new(:ok, 42) failure = Result.new(:error, "not found") [success, failure].each do |result| case result.type when :ok then puts "Got #{result.value}" when :error then puts "Error: #{result.value}" end end
enum Outcome { case success(Int) case failure(String) } let results: [Outcome] = [.success(42), .failure("not found")] for result in results { switch result { case .success(let value): print("Got \(value)") case .failure(let message): print("Error: \(message)") } }

Swift enum cases can carry associated data of any type. This is one of Swift's most powerful features β€” it elegantly replaces many class hierarchies. Swift's built-in Result<Success, Failure> and Optional<Wrapped> types are implemented exactly this way.

Error Handling
throw / rescue
class ParseError < StandardError; end def parse_age(str) age = Integer(str) raise ParseError, "Must be positive" if age < 0 age rescue ArgumentError raise ParseError, "Not a number: #{str}" end begin puts parse_age("25") puts parse_age("abc") rescue ParseError => error puts "Error: #{error.message}" end
enum ParseError: Error { case notANumber(String), mustBePositive } func parseAge(_ str: String) throws -> Int { guard let age = Int(str) else { throw ParseError.notANumber(str) } guard age >= 0 else { throw ParseError.mustBePositive } return age } do { print(try parseAge("25")) print(try parseAge("abc")) } catch ParseError.notANumber(let str) { print("Error: Not a number: \(str)") } catch { print("Error: \(error)") }

Swift uses throws/throw/try/do-catch where Ruby uses raise/rescue/begin-end. Throwing functions must be marked throws β€” callers see at compile time that an error can occur and must handle it explicitly with try.

Extensions
Extending built-in types
# Ruby: open classes (monkey patching) class Integer def factorial return 1 if self <= 1 self * (self - 1).factorial end end class String def palindrome? = self == self.reverse end puts 5.factorial puts "racecar".palindrome?
extension Int { func factorial() -> Int { self <= 1 ? 1 : self * (self - 1).factorial() } } extension String { var isPalindrome: Bool { self == String(self.reversed()) } } print(5.factorial()) print("racecar".isPalindrome)

Swift extension is the structured equivalent of Ruby's open classes. Extensions can add methods, computed properties, and protocol conformances to any type β€” including built-ins β€” without modifying the original source. Unlike Ruby, Swift extensions cannot add stored (instance variable) properties.

Concurrency
Async / await
thread1 = Thread.new { "result 1" } thread2 = Thread.new { "result 2" } puts thread1.value puts thread2.value
import Foundation func fetchData(_ label: String) async -> String { try? await Task.sleep(nanoseconds: 10_000_000) return "result \(label)" } let results = await withTaskGroup(of: String.self) { group in group.addTask { await fetchData("1") } group.addTask { await fetchData("2") } var collected: [String] = [] for await value in group { collected.append(value) } return collected.sorted() } print(results.joined(separator: "\n"))

Swift 6 has structured concurrency built into the language. async/await is similar to modern Ruby using the Async gem. Swift 6's strict concurrency mode makes data races compile-time errors β€” a major safety improvement over Ruby's GIL-based threading model.

⚠ Gotchas for Rubyists
<code>let</code> means immutable constant
# In Ruby, let is only an RSpec helper β€” all variables mutate freely x = 1 x = 2 # fine x += 1 # fine puts x
let x = 1 // x = 2 // compile error: cannot assign to let constant // x += 1 // compile error var y = 1 y = 2 // fine: var is mutable y += 1 print(y)

The most common early stumble: Swift's let creates an immutable constant that the compiler enforces at compile time. In Ruby, let has no special meaning outside RSpec. Declare everything with let by default β€” the compiler will tell you when you actually need var.

Range operators are reversed
puts (1..5).to_a.inspect # [1,2,3,4,5] inclusive (..) puts (1...5).to_a.inspect # [1,2,3,4] exclusive (...)
print(Array(1...5)) // [1,2,3,4,5] inclusive (...) print(Array(1..<5)) // [1,2,3,4] exclusive (..<) // Ruby's ... and Swift's ... mean OPPOSITE things!

⚠ High off-by-one risk: Ruby's ... excludes the endpoint; Swift's ... includes it. For exclusive ranges, Swift uses ..< (half-open). They look nearly identical but mean opposite things β€” double-check every range when porting logic between languages.

No implicit nil β€” everything must be initialized
class Config attr_accessor :timeout, :host # @timeout and @host default to nil β€” no error end cfg = Config.new puts cfg.timeout.inspect # nil puts cfg.host.inspect # nil
struct Config { var timeout: Int // must be initialized β€” no default nil var host: String? // ? explicitly declares "may be nil" } // let cfg = Config() // compile error: missing timeout let cfg = Config(timeout: 30, host: nil) print(cfg.timeout) // 30 print(cfg.host as Any) // nil

Swift requires every non-optional property to have a value before the object is used. You cannot leave a property uninitialised and silently get nil back. If a property is genuinely optional, declare it as Type?. This eliminates entire categories of "undefined method for nil:NilClass" errors.

Structs are copied, not shared
numbers = [1, 2, 3] copy = numbers # same array object! copy << 4 puts numbers.inspect # [1, 2, 3, 4] β€” mutated! puts numbers.equal?(copy) # true β€” same object
var numbers = [1, 2, 3] // Array is a struct β€” value type var copy = numbers // COPY created here copy.append(4) print(numbers) // [1, 2, 3] β€” unchanged print(copy) // [1, 2, 3, 4]

Swift's Array, Dictionary, and String are all structs (value types), so assignment creates an independent copy. Ruby's equivalent types are objects (reference types), so assignment shares the reference. Mutating a Swift array after assigning it to another variable does not affect the original.