Basics & Running
Hello, World
void main() {
print('Hello, World!');
} print("Hello, World!") Swift permits statements at file scope, so a script has no entry function at all — the top-level code just runs, top to bottom. This is a bigger departure than it looks: any helper
func or struct is declared alongside the top-level statements that use them, and there is no void main() to wrap them in.final/var becomes let/var
void main() {
final answer = 42;
var counter = 0;
counter += 1;
print(answer + counter);
} let answer = 42
var counter = 0
counter += 1
print(answer + counter) Dart's
final is let and var is var. The meaning of let is stronger than final when it holds a value type, though: a let on a struct makes the whole value immutable, including its properties, whereas Dart's final only fixes the binding to a mutable object. That distinction drives the next section.String interpolation
void main() {
final name = 'Ada';
final year = 1843;
print('$name wrote the first program in $year.');
print('Next year: ${year + 1}');
} let name = "Ada"
let year = 1843
print("\(name) wrote the first program in \(year).")
print("Next year: \(year + 1)") The idea is the same but the syntax is unusual: Swift wraps every interpolation in
\(…) — a backslash and parentheses — with no bare-variable shorthand like Dart's $name. There is one form for both a simple variable and a full expression, which is at least consistent.Numbers: no implicit conversion
void main() {
int whole = 7 ~/ 2; // integer division operator
double exact = 7 / 2; // / always yields a double
double mixed = 3 + 0.5; // int + double promotes automatically
print('$whole $exact $mixed');
} let whole = 7 / 2 // Int / Int truncates -> 3
let exact = 7.0 / 2 // force a Double for 3.5
let count = 3
let mixed = Double(count) + 0.5 // NO implicit Int->Double; convert explicitly
print("\(whole) \(exact) \(mixed)") Two changes bite a Dart programmer. There is no
~/: / on two Ints already truncates, and you force floating point by making an operand a Double. And Swift does no implicit numeric conversion — Double(count) + 0.5 must convert explicitly, where Dart quietly promotes an int to a double. There is also no num supertype.Value Types: struct vs class
struct copies; class references
The deepest shift from Dart, which has only reference types. A Swift
struct is a value: assigning it makes an independent copy.class Point {
int x, y;
Point(this.x, this.y);
}
void main() {
final first = Point(1, 2);
final second = first; // same object — a reference
second.x = 99;
print(first.x); // 99 — aliasing
} struct Point {
var x: Int
var y: Int
}
var first = Point(x: 1, y: 2)
var second = first // an independent COPY
second.x = 99
print(first.x) // 1 — the copy did not touch first A Dart class assignment aliases one object, so mutating
second changes first. A Swift struct assignment copies, so the two are independent — the model Dart has no equivalent for. Swift also has class (a reference type, like every Dart class), but the community default is struct: reach for class only when you specifically need shared identity.let structs & mutating methods
class Counter {
int value = 0;
void increment() => value++;
}
void main() {
final counter = Counter(); // final, but the object still mutates
counter.increment();
print(counter.value); // 1
} struct Counter {
var value = 0
mutating func increment() { // must be marked 'mutating'
value += 1
}
}
var counter = Counter() // must be var to call a mutating method
counter.increment()
print(counter.value) // 1 Because a
struct is a value, a method that changes it must be marked mutating, and you can only call such a method on a var — a let struct is fully frozen, properties and all. This is stricter than Dart, where final counter still lets you call mutating methods because final only pins the reference, not the object's contents.Optionals
Nullable types → Optionals
String? findName(int id) => id == 1 ? 'Ada' : null;
void main() {
final name = findName(1);
if (name != null) {
print(name.length); // promotion after the null check
}
} func findName(_ id: Int) -> String? {
return id == 1 ? "Ada" : nil
}
if let name = findName(1) { // optional binding unwraps into a new constant
print(name.count)
} A nullable
String? is a Swift Optional (also written String?), and the absent value is nil. The idiomatic unwrap differs from Dart's flow promotion: if let name = findName(1) binds a non-optional name for the block only if the value is present. Swift optionals are sound, like Dart's null safety — there is no platform-type escape hatch.?? and ! carry over; guard is new
int? parsePort(String text) => int.tryParse(text);
void main() {
final port = parsePort('8080') ?? 80; // ?? default
print(port);
final forced = parsePort('9090')!; // ! asserts non-null
print(forced);
} func portFor(_ text: String) -> Int? { Int(text) }
func openOr80(_ text: String) -> Int {
guard let port = portFor(text) else { return 80 } // early-exit unwrap
return port
}
let port = portFor("8080") ?? 80 // ?? — same as Dart
print(port)
let forced = portFor("9090")! // ! — force unwrap, same as Dart
print(forced)
print(openOr80("bad")) Two operators port over unchanged:
?? supplies a default and ! force-unwraps (trapping at run time if nil, exactly like Dart's !). Swift adds one tool Dart lacks — guard let … else { return } — which unwraps into the enclosing scope and forces you to handle the absent case up front, keeping the happy path unindented.late → lazy var
class Report {
late final String body = _render(); // computed once, on first read
String _render() => 'expensive';
}
void main() {
final report = Report();
print(report.body);
} struct Report {
lazy var body: String = Report.render() // computed once, on first read
static func render() -> String { "expensive" }
}
var report = Report()
print(report.body) Dart's lazy
late (compute on first access) is Swift's lazy var, which likewise defers the initializer until the property is first read. Note it must be a var (reading it mutates the stored state), so a lazy property makes its containing struct mutable. Dart's other late — deferred-but-non-lazy — maps to a Swift implicitly-unwrapped optional (var x: String!).ARC, Not GC
Reference counting, not a collector
Dart has a tracing garbage collector, so object lifetime is invisible. Swift uses ARC — automatic reference counting — which frees a class instance the moment its last reference goes away.
class Logger {
final String name;
Logger(this.name);
}
void main() {
var logger = Logger('main');
logger = Logger('other'); // the first Logger is now unreachable;
print(logger.name); // Dart's GC will reclaim it eventually
} class Logger {
let name: String
init(name: String) { self.name = name }
deinit { print("freeing \(name)") } // ARC calls this deterministically
}
var logger: Logger? = Logger(name: "main")
logger = Logger(name: "other") // "freeing main" prints right here
print(logger!.name) ARC frees a class instance deterministically — the instant its reference count hits zero — which is why the
deinit for main runs the moment it is replaced, something a Dart finalizer can never promise. ARC applies only to classes; value types (structs, enums) are copied and need no counting. The cost is that ARC cannot break reference cycles on its own, which the next concept addresses.Retain cycles & weak
// Dart's tracing GC reclaims cycles automatically — no special
// annotation is ever needed for a parent <-> child back-reference.
class Parent { Child? child; }
class Child { Parent? parent; }
void main() {
final parent = Parent();
final child = Child();
parent.child = child;
child.parent = parent; // a cycle; Dart's GC still frees both
print('linked');
} final class Parent { var child: Child? }
final class Child {
weak var parent: Parent? // 'weak' does not raise the reference count
}
let parent = Parent()
let child = Child()
parent.child = child
child.parent = parent // without 'weak' this cycle would leak under ARC
print("linked") Here Dart is simpler: a tracing collector reclaims cyclic garbage automatically, so a parent/child back-reference needs no thought. Under ARC a strong cycle never reaches zero and leaks, so you must mark one side
weak (auto-nils when the target is freed) or unowned (assumed to outlive you). Learning where to place weak — especially in closures that capture self — is the main new discipline for a Dart programmer.Enums with Payloads
Sealed class → enum with payloads
In Dart an
enum cannot carry per-case data, so you reach for a sealed class. Swift folds both jobs into one enum whose cases hold associated values.sealed class Shape {}
class Circle extends Shape { final double r; Circle(this.r); }
class Rect extends Shape { final double w, h; Rect(this.w, this.h); }
double area(Shape shape) => switch (shape) {
Circle(:final r) => 3.14159 * r * r,
Rect(:final w, :final h) => w * h,
};
void main() => print(area(Circle(2))); enum Shape {
case circle(r: Double)
case rect(w: Double, h: Double)
}
func area(_ shape: Shape) -> Double {
switch shape {
case .circle(let r): return 3.14159 * r * r
case .rect(let w, let h): return w * h
}
}
print(area(.circle(r: 2))) A Swift
enum case can carry associated values, so the whole Dart sealed hierarchy collapses into one enum Shape — exactly the feature Dart's enum lacks. A switch over it is exhaustive (omit a case and it fails to compile) and binds the payload inline with case .circle(let r), which is close to Dart's object pattern. Swift enums also do the plain job via raw values (enum Direction: String).Enums with methods
enum Planet {
earth(9.8),
mars(3.7);
final double gravity;
const Planet(this.gravity);
double weight(double mass) => mass * gravity;
}
void main() {
print(Planet.mars.weight(70));
} enum Planet: Double {
case earth = 9.8
case mars = 3.7
func weight(_ mass: Double) -> Double {
return mass * rawValue
}
}
print(Planet.mars.weight(70)) Dart's enhanced enum with a per-constant value becomes a Swift raw-value enum:
enum Planet: Double attaches a Double to each case, read back through the built-in rawValue property. Methods are declared inside the enum in both languages. Reach for a raw-value enum when each case maps to one constant, and associated values (previous concept) when cases carry structured data.Classes & Protocols
Constructors → init
class Rectangle {
final int width;
final int height;
Rectangle(this.width, this.height);
int get area => width * height;
}
void main() {
print(Rectangle(3, 4).area);
} struct Rectangle {
let width: Int
let height: Int
var area: Int { width * height } // computed property
}
print(Rectangle(width: 3, height: 4).area) A Swift constructor is
init, but a struct gives you a memberwise initializer for free — Rectangle(width:height:) here needs no code, and every argument is labeled by default. A computed property uses a braces-block getter, var area: Int { … }. There is no new keyword, so construction reads like a function call, as in Dart.Interfaces → protocols
abstract class Speaker {
String speak();
}
class Dog implements Speaker {
@override
String speak() => 'Woof';
}
void main() => print(Dog().speak()); protocol Speaker {
func speak() -> String
}
struct Dog: Speaker {
func speak() -> String { "Woof" }
}
print(Dog().speak()) Swift's
protocol is Dart's interface/abstract contract, and conformance is declared after the colon (struct Dog: Speaker). The big idea Swift adds is that a struct or enum — not just a class — can conform, so protocols are the primary abstraction rather than base classes. Unlike Dart, every class does not automatically define an implicit interface.Mixins → protocol extensions
mixin Greetable {
String get who;
String greet() => 'Hello, $who';
}
class Person with Greetable {
@override
final String who;
Person(this.who);
}
void main() => print(Person('Ada').greet()); protocol Greetable {
var who: String { get }
}
extension Greetable { // default implementation
func greet() -> String { "Hello, \(who)" }
}
struct Person: Greetable { let who: String }
print(Person(who: "Ada").greet()) Dart mixes shared behavior in with
with. Swift's equivalent is a protocol extension: declare the requirement in the protocol, then supply a default method in an extension Greetable that every conformer inherits. This "protocol-oriented" style is Swift's answer to both mixins and abstract base classes, and it works for value types too.No cascade operator
class Basket {
final List<String> items = [];
void add(String item) => items.add(item);
}
void main() {
final basket = Basket()
..add('apple')
..add('pear')
..add('plum'); // cascade fires several calls at one receiver
print(basket.items);
} struct Basket {
var items: [String] = []
mutating func add(_ item: String) { items.append(item) }
}
var basket = Basket()
basket.add("apple")
basket.add("pear")
basket.add("plum") // no cascade — repeat the receiver
print(basket.items) Swift has no cascade operator, so there is no direct stand-in for Dart's
... You repeat the receiver, or — the closest idiom — configure inside a closure: let basket = { var b = Basket(); b.add("apple"); return b }(). For class-based builder APIs you can still hand-roll a fluent chain by returning self, but there is no language-level cascade.Tuples & Destructuring
Records → tuples (with names)
(int, String) minWithLabel(List<int> numbers) {
final sorted = [...numbers]..sort();
return (sorted.first, 'sorted');
}
void main() {
final (smallest, note) = minWithLabel([5, 2, 9]);
print('$smallest / $note');
} func minWithLabel(_ numbers: [Int]) -> (Int, String) {
return (numbers.sorted().first!, "sorted")
}
let (smallest, note) = minWithLabel([5, 2, 9])
print("\(smallest) / \(note)") This is one of the closest matches on the page: a Dart positional record
(int, String) is a Swift tuple (Int, String), destructured the same way with let (a, b) = …. Swift tuples can also name their elements — (x: Int, y: Int), accessed as point.x — mirroring Dart's named records almost exactly. The difference is scale: Swift tuples are best kept small, with a struct preferred past two or three fields.Named-tuple returns
({int width, int height}) screen() => (width: 1920, height: 1080);
void main() {
final size = screen();
print('${size.width} x ${size.height}');
} func screen() -> (width: Int, height: Int) {
return (width: 1920, height: 1080)
}
let size = screen()
print("\(size.width) x \(size.height)") Dart's named record fields map directly to a Swift named tuple, and both read their members by label (
size.width). It is a lightweight way to return a couple of labeled values without declaring a type — but once the shape is reused or grows, both communities steer you toward a real struct/record type instead of passing tuples around.Functions & Argument Labels
Arguments are labeled by default
A Swift surprise with no Dart parallel: parameters are labeled at the call site by default, and a function's name effectively includes those labels.
String move(int x, int y, {String unit = 'px'}) =>
'move to $x,$y in $unit';
void main() {
print(move(3, 4)); // positional
print(move(3, 4, unit: 'em')); // named optional
} func move(x: Int, y: Int, unit: String = "px") -> String {
return "move to \(x),\(y) in \(unit)"
}
print(move(x: 3, y: 4)) // labels are REQUIRED by default
print(move(x: 3, y: 4, unit: "em")) Where Dart parameters are positional unless wrapped in
{ }, Swift labels every argument by default — move(x: 3, y: 4) — and the labels are part of the function's identity. To make one positional you write an underscore before it (func move(_ x: Int)), and you can give a separate external label (func greet(to name: String) called greet(to: "Ada")). Defaults work as in Dart.Closures & trailing-closure sugar
void main() {
final numbers = [1, 2, 3, 4];
final evens = numbers.where((n) => n.isEven).toList();
final doubled = numbers.map((n) => n * 2).toList();
print('$evens $doubled');
} let numbers = [1, 2, 3, 4]
let evens = numbers.filter { $0 % 2 == 0 } // trailing closure + $0 shorthand
let doubled = numbers.map { $0 * 2 }
print("\(evens) \(doubled)") Swift closures use braces, and when a closure is a function's last argument it moves outside the parentheses — the trailing-closure form, so
filter { … }. Inside, positional arguments are available as $0, $1, … without naming them, tighter than Dart's (n) =>. The methods rename slightly: Dart's where is filter, and map is the same.Extensions (both have them)
extension NumberWords on int {
String get spelledOut => this == 1 ? 'one' : 'many';
}
void main() {
print(3.spelledOut); // 'many'
} extension Int {
var spelledOut: String { self == 1 ? "one" : "many" }
}
print(3.spelledOut) // "many" Extensions survive the trip. Swift's
extension Int { … } adds a computed property to an existing type just like Dart's extension … on int, and self plays the role of Dart's this. Swift goes further: an extension can also add protocol conformance to a type you do not own, which is central to its protocol-oriented style.switch & Pattern Matching
switch & value binding
String describe(int n) => switch (n) {
0 => 'zero',
1 || 2 || 3 => 'small',
final v when v < 0 => 'negative',
_ => 'large',
};
void main() {
print(describe(2));
print(describe(-5));
} func describe(_ n: Int) -> String {
switch n {
case 0: return "zero"
case 1...3: return "small"
case let v where v < 0: return "negative"
default: return "large"
}
}
print(describe(2))
print(describe(-5)) Both languages have an exhaustive, pattern-based
switch, and they are strikingly similar. Swift uses case/default and ranges (1...3) where Dart uses =>/_ and || alternatives, and both support a value binding with a guard: Swift's case let v where v < 0 is Dart's final v when v < 0. Swift cases do not fall through by default, so no break is needed.Tuple patterns
void main() {
final point = (2, 0);
final result = switch (point) {
(0, 0) => 'origin',
(final x, 0) => 'on x-axis at $x',
(0, final y) => 'on y-axis at $y',
_ => 'elsewhere',
};
print(result);
} let point = (2, 0)
let result: String
switch point {
case (0, 0): result = "origin"
case (let x, 0): result = "on x-axis at \(x)"
case (0, let y): result = "on y-axis at \(y)"
default: result = "elsewhere"
}
print(result) Swift matches tuples structurally, binding pieces with
let and matching literals in place — case (let x, 0) is Dart's (final x, 0) almost verbatim. This is the same destructuring-while-matching that Dart 3 patterns brought, and it is one of the areas where the two languages feel most alike, down to wildcard positions and value bindings.Collections are Values
Array/Dictionary/Set are values
A consequence of value types that surprises every Dart programmer: Swift's
Array, Dictionary, and Set are structs, so assigning one copies it.void main() {
final first = [1, 2, 3];
final second = first; // same list — a reference
second.add(4);
print(first); // [1, 2, 3, 4] — aliasing
} var first = [1, 2, 3]
var second = first // an independent COPY (copy-on-write)
second.append(4)
print(first) // [1, 2, 3] — first is untouched In Dart a
List is a reference, so second.add(4) is visible through first. In Swift the built-in collections are value types: second is a copy, and mutating it leaves first alone. Swift implements this efficiently with copy-on-write, so the duplication only happens on the first mutation — but the semantics you must reason about are full value copies.Maps → Dictionary
void main() {
final ages = {'Ada': 36, 'Grace': 45};
print(ages['Ada']); // 36
print(ages['Nobody']); // null
for (final entry in ages.entries) {
print('${entry.key}: ${entry.value}');
}
} let ages = ["Ada": 36, "Grace": 45]
print(ages["Ada"] ?? 0) // subscript returns an Optional
print(ages["Nobody"] ?? 0) // nil for a missing key
for (name, age) in ages.sorted(by: { $0.key < $1.key }) {
print("\(name): \(age)")
} A Swift dictionary literal uses colons,
["Ada": 36], and indexing returns an Optional — ages["Ada"] is Int?, so you commonly pair it with ??, where Dart just hands back null. Iteration yields (key, value) tuples you can destructure in the for. Like all Swift collections a Dictionary is unordered, so this example sorts for stable output.fold/reduce
void main() {
final numbers = [1, 2, 3, 4];
final total = numbers.fold<int>(0, (sum, each) => sum + each);
final maxValue = numbers.reduce((a, b) => a > b ? a : b);
print('$total $maxValue');
} let numbers = [1, 2, 3, 4]
let total = numbers.reduce(0) { sum, each in sum + each } // like Dart's fold
let maxValue = numbers.max()! // built-in
print("\(total) \(maxValue)") Swift's
reduce(initial) { … } takes a seed of any type, so it plays the role of Dart's fold. There is no separate seedless reduce like Dart's; for the common aggregates Swift ships dedicated methods — max(), min(), and (for numbers) reduce(0, +) — which return optionals and read more clearly than a hand-written fold.async/await & Actors
Future/async → async/await
Future<int> fetchCount() async {
await Future.delayed(Duration(milliseconds: 10));
return 42;
}
void main() async {
final count = await fetchCount();
print(count);
} func fetchCount() async -> Int {
try? await Task.sleep(for: .milliseconds(10))
return 42
}
let count = await fetchCount()
print(count) This maps almost one to one: a Dart
async function returning Future<int> is a Swift func … async -> Int, and await means the same on both sides. Two spelling notes: Swift writes the effect in the signature (async, before the return arrow), and a delay is Task.sleep. Swift also permits top-level await, so no wrapper is needed here.Future.wait → async let
Future<int> load(int id) async => id * 10;
void main() async {
final results = await Future.wait([load(1), load(2), load(3)]);
print(results); // [10, 20, 30]
} func load(_ id: Int) async -> Int { id * 10 }
async let a = load(1) // all three start concurrently
async let b = load(2)
async let c = load(3)
let results = await [a, b, c]
print(results) // [10, 20, 30] To run work concurrently and then await it together, Swift offers
async let: each binding starts immediately, and awaiting them (here in an array literal) joins the results — the structured-concurrency equivalent of Dart's Future.wait. For a dynamic number of tasks Swift uses a TaskGroup instead, where Dart would map over a list into Future.wait.Isolates → actors
Both languages protect shared state by isolating it, but the models differ. Dart isolates share no memory; Swift
actors share one heap but serialize access to their own mutable state.import 'dart:isolate';
Future<int> heavySum(int n) {
// A separate isolate: its own heap, arguments copied, nothing shared.
return Isolate.run(() {
var total = 0;
for (var i = 1; i <= n; i++) total += i;
return total;
});
}
void main() async {
print(await heavySum(1000)); // 500500
} actor Accumulator {
private var total = 0
func add(_ n: Int) { total += n } // access is serialized by the actor
func sum() -> Int { total }
}
let accumulator = Accumulator()
for i in 1...1000 {
await accumulator.add(i) // 'await' — crossing into the actor
}
print(await accumulator.sum()) // 500500 A Dart
Isolate has a private heap and communicates by copying messages, so there is no shared state to race. A Swift actor instead owns its mutable state and serializes every access, so calls into it are awaited (they may suspend) and the compiler stops you from touching its state directly. Different mechanisms, same goal — safe concurrent state — and both spare you manual locks for the common case.Errors
try/catch → throws & do-catch
class BadInput implements Exception {
final String message;
BadInput(this.message);
}
int parse(String text) {
final value = int.tryParse(text);
if (value == null) throw BadInput('not a number');
return value;
}
void main() {
try {
print(parse('nope'));
} catch (error) {
print('caught: $error');
}
} struct BadInput: Error { let message: String }
func parse(_ text: String) throws -> Int {
guard let value = Int(text) else { throw BadInput(message: "not a number") }
return value
}
do {
print(try parse("nope")) // 'try' is required at the call site
} catch {
print("caught: \(error)") // 'error' is bound implicitly
} Swift marks a throwing function with
throws in its signature and forces the caller to write try at the call site — a visibility Dart does not require. A conforming error is any type adopting the empty Error protocol. The handler is do { … } catch { … }, and the caught value is available as the implicit error binding without you naming it.try? and try!
int parse(String text) {
final value = int.tryParse(text);
if (value == null) throw FormatException('bad');
return value;
}
void main() {
// Dart's idiom for "give me null on failure" is a manual try/catch.
int? maybe;
try {
maybe = parse('nope');
} catch (_) {
maybe = null;
}
print(maybe ?? -1);
} struct BadInput: Error {}
func parse(_ text: String) throws -> Int {
guard let value = Int(text) else { throw BadInput() }
return value
}
let maybe = try? parse("nope") // nil on any thrown error -> Int?
print(maybe ?? -1)
let sure = try! parse("42") // trap on error; use only when certain
print(sure) Swift adds two terse call-site forms that Dart lacks.
try? turns a throw into nil, collapsing a whole try/catch into an optional — the pattern the Dart column has to write by hand. try! asserts the call cannot fail and traps if it does, the error-handling cousin of force-unwrap !. Both keep the marked-try discipline while trading detail for brevity.Modules & Tooling
Libraries → modules & import
// geometry.dart
// double areaOfCircle(double r) => 3.14159 * r * r;
//
// main.dart
import 'geometry.dart';
void main() {
print(areaOfCircle(2));
} // Swift imports whole MODULES, not files: every .swift file in a target
// shares one namespace, so no import is needed between them.
import Foundation // the standard library module
func areaOfCircle(_ r: Double) -> Double { 3.14159 * r * r }
print(areaOfCircle(2)) Swift's unit of import is a module (a whole library or target), not an individual file, so files within the same target see each other with no
import at all — quite different from Dart's per-file import. You import Foundation or another package by module name. Visibility is controlled with public/internal/private keywords rather than Dart's leading-underscore convention. The Dart side references a second file, so its column is display-only.pub → Swift Package Manager
// pubspec.yaml
// dependencies:
// http: ^1.2.0
//
// Then: dart pub get
// Then: import 'package:http/http.dart' as http;
void main() {
print('Dependencies are declared in pubspec.yaml and fetched by pub.');
} // Package.swift
// dependencies: [
// .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0")
// ]
//
// Then: swift build
print("Dependencies live in Package.swift and are fetched by SwiftPM.") Dart's
pubspec.yaml plus dart pub get corresponds to a Package.swift manifest plus swift build using the Swift Package Manager. The manifest is itself written in Swift (not YAML), and dependencies are resolved straight from Git URLs and tags rather than from a central registry like pub.dev.