Swift Fundamentals
History
Let’s rewind to 2014. Out of nowhere, Apple unveiled Swift, a new programming language that didn’t just iterate on Objective-C, but promised to reinvent how we write code for Apple platforms. Swift wasn’t just another language. It was a statement: type safety, fewer runtime errors, and a relentless focus on developer happiness. I was skeptical, but i just wanna try it out.
The Evolution
Swift didn’t stand still. Swift 2.0 (2015) brought dramatic changes, error handling, protocol extensions, better availability checking. Every release since has doubled down on clarity and power. Now, at version 6.1, Swift isn’t just for iOS: it’s a cross-platform, server-ready, open-source powerhouse. Swift is Apple’s baby, a modern language for building iOS, macOS, and even server-side apps. It borrows the best ideas from languages like Ruby, Python, and C#, but it’s not just a mashup. Swift is designed for safety, speed, and expressiveness, so you can write code that’s both powerful and readable. If you’ve ever been burned by a mysterious crash or a null pointer exception, you’ll appreciate Swift’s relentless focus on preventing entire classes of bugs before your code even runs.
And the ecosystem? Swiftly, the new version manager, finally makes dependency and toolchain management painless. No more wrestling with mismatched library versions. Add in GRPC support and stellar VSCode extensions, and Swift feels modern in a way Objective-C never did.
I will be exploring swift basics here, once the basics click, Swift has even more to offer: protocols for flexible code, generics for reusability, concurrency for speed, and even server-side. But start here. Wrestle with the basics. The rest will follow.
The Building Blocks
Swift’s core types are refreshingly few and clear:
Int
: Whole numbers, signed or unsigned, with automatic type inference.Double
andFloat
: For decimalsDouble
for precision,Float
for speed.String
: Text, as you’d expect.Bool
:true
orfalse
.Character
: Single Unicode characters.- Custom types: Structs, enums, and classes you define yourself.
Declaring variables is beautifully simple: var
for things that change, let
for things that don’t. No more var
, let
, const
, and whatever else JavaScript throws at you. Just two choices, and the compiler will let you know if you mess up.
var age: Int = 30
let birthYear = 1995
You can even declare a variable without an initial value, as long as you specify its type:
var temperature: Double
temperature = 21.5
Collections: Arrays, Sets, and Dictionaries
Swift’s collections are the Swiss Army knives of data handling:
- Array: Ordered, indexed, and type-safe. Utility methods like
count
,first
,last
,append
,insert
,reverse
,shuffle
make them a joy to use. - Set: Unordered, unique elements, blazing fast membership checks. Use
insert
andremove
to manage contents. - Dictionary: Key-value pairs. Add or update with subscript syntax, remove by setting a value to
nil
.
var numbers = [1, 2, 3]
numbers.append(4)
var uniqueNumbers: Set = [1, 2, 3]
uniqueNumbers.insert(4)
let numbers = [1, 2, 3, 4]
let doubled = numbers.map { $0 * 2 } // [2, 4, 6, 8]
var capitals = ["Kenya": "Nairobi"]
capitals["Uganda"] = "Kampala"
capitals["Kenya"] = nil
Absolutely! Here’s a paragraph you can add about Functions, Parameters, and Return Values, in the same thoughtful, conversational style:
Functions
Declaring a function means giving it a name, defining what inputs (parameters) it needs, and specifying what it spits out (return value).
func greet(name: String, withGreeting greeting: String = "Hello") -> String {
return "\(greeting), \(name)!"
}
print(greet(name: "Alice")) // Prints: Hello, Alice!
print(greet(name: "Bob", withGreeting: "Hi")) // Prints: Hi, Bob!
Error Handling
Now, Swift also has a robust way to handle errors through throwing functions. These are functions that can signal something went wrong by “throwing” an error, instead of returning a value or crashing silently. You mark these functions with the throws
keyword, and inside, you can throw
specific errors defined by you, usually using an enum that conforms to the Error
protocol.
enum PasswordError: Error {
case tooShort
case tooObvious
}
func checkPassword(_ password: String) throws -> String {
if password.count < 5 {
throw PasswordError.tooShort
}
if password == "12345" {
throw PasswordError.tooObvious
}
return "Password is good!"
}
do {
let result = try checkPassword("12345")
print(result)
} catch PasswordError.tooShort {
print("Password is too short.")
} catch PasswordError.tooObvious {
print("Password is too obvious.")
}
Closures and Passing Functions into Functions
The first time i saw a closure in Swift, I was surprised, looked like a function, but it wasn’t declared with func
, and it seemed to pop up in places where a “normal” function just wouldn’t fit. But closures are one of Swift’s most known features self-contained blocks of code you can pass around, store in variables, and hand off to other functions like a baton in a relay race.
What Is a Closure
A closure in Swift is a chunk of functionality, think of it as an unnamed function, that you can assign to a variable or pass as an argument. Closures can capture and remember values from their surrounding context, which makes them incredibly flexible for things like callbacks, completion handlers, and functional programming patterns.
Here’s the basic syntax:
let greet = {
print("Hello, World!")
}
greet() // Prints: Hello, World!
You can also write closures that take parameters and return values:
let add: (Int, Int) -> Int = { (a, b) in
return a + b
}
print(add(2, 3))
print(add(2, 3))
// 5
// 5
Swift’s closure syntax can be simplified even further when the context is clear, letting you write concise, readable code.
Passing Closures as Function Parameters
Where closures really shine is as parameters to functions. This lets you customize a function’s behavior without rewriting it. For example:
func grabLunch(search: () -> Void) {
print("Let's go out for lunch")
search()
}
grabLunch {
print("Alfredo's Pizza: 2 miles away")
}
// Let's go out for lunch
// Alfredo's Pizza: 2 miles away
Here, grabLunch
takes a closure as an argument and calls it inside the function. This pattern is everywhere in Swift-from sorting arrays to handling asynchronous operations.
Structs, Computed Properties, and Property Observers
If there’s one Swift feature that made me rethink how I structure my code, it’s the humble struct. Unlike classes, structs in Swift are value types, meaning copies, not references, get passed around. This single fact has saved me from countless accidental side-effects. But structs are just the beginning; the real magic happens when you combine them with computed properties and property observers.
Structs: Your Lightweight Data Models
A struct in Swift is a container for related data, often used for models that don’t need inheritance. Each property you declare is either a stored property (it holds data directly) or a computed property (it calculates a value on the fly).
struct Person {
var firstName: String
var lastName: String
var age: Int
}
let person = Person(firstName: "John", lastName: "Doe", age: 30)
print(person.firstName) // Outputs: John
Computed Properties: Logic Without Storage
Computed properties are where Swift starts to feel like a language that trusts you to write expressive, readable code. Unlike stored properties, a computed property doesn’t keep its own value, it calculates it every time you access it. You define a getter (and optionally a setter) to control how the value is derived or changed.
struct Circle {
var radius: Double
var circumference: Double {
get { 2 * .pi * radius }
set { radius = newValue / (2 * .pi) }
}
}
var circle = Circle(radius: 5)
print(circle.circumference) // 31.415...
circle.circumference = 62.83
print(circle.radius) // Updates radius based on new circumference
You can also create read, only computed properties by omitting the setter, perfect for values derived from other properties:
struct Rectangle {
var width: Double
var height: Double
var area: Double { width * height }
}
let rect = Rectangle(width: 4, height: 5)
print(rect.area) // 20
Property Observers: Watching for Change
Sometimes, you want to react when a property’s value changes, maybe to update the UI or log a message. That’s where property observers come in. Attach willSet
and didSet
to a stored property to run code before or after its value changes. They don’t work on computed properties, since those don’t store data.
struct Counter {
var count: Int = 0 {
willSet {
print("About to set count to \(newValue)")
}
didSet {
print("Count changed from \(oldValue) to \(count)")
}
}
}
var counter = Counter()
counter.count = 10
// Output:
// About to set count to 10
// Count changed from 0 to 10
Access Control
Swift offers five main access levels to control visibility and accessibility of your classes, structs, methods, and properties:
public
: Accessible from anywhere, even outside your module or framework.internal
(the default): Accessible anywhere within the same module (think: your app or framework).fileprivate
: Accessible only within the same Swift source file.private
: Accessible only within the enclosing declaration (class, struct, or extension).open
: Likepublic
, but also allows subclassing and overriding outside the module (mostly relevant for classes).
This hierarchy lets you shield implementation details and expose only what’s necessary. For example, marking a method private
means no other part of your code, even subclasses, can touch it. This can prevent accidental misuse or bugs creeping in from unexpected places.
class Vehicle {
public func startEngine() {
print("Engine started")
}
private func checkFuel() {
print("Fuel level OK")
}
}
Here, startEngine()
is accessible everywhere, but checkFuel()
is locked inside Vehicle
. Trying to call checkFuel()
from outside triggers a compiler error.
Static Properties and Methods
Static members are another cornerstone of Swift’s design. When you declare a property or method as static
, it belongs to the type itself, not any particular instance. This is great for shared constants, utility functions, or counters that track usage across all instances.
struct Math {
static let pi = 3.14159
static func square(_ x: Int) -> Int {
return x * x
}
}
print(Math.pi) // 3.14159
print(Math.square(5)) // 25
You don’t need to create a Math
object to use pi
or square
, you call them directly on the type. This pattern keeps your code organized and avoids unnecessary object creation.
At first, access control and static members felt like extra syntax to memorize. But they’re really about intentionality. When you mark something private
, you’re telling your future self and collaborators: “This is an internal detail. Don’t touch unless you know what you’re doing.” When you use static
, you’re signaling: “This belongs to the concept, not the thing.”
These tools help you build safer, clearer, and more maintainable codebases. They’re part of Swift’s broader philosophy: code should be explicit, predictable, and easy to reason about.
Struct vs Classes
When should you reach for a struct, and when does a class make more sense in Swift? I’m still fascinated by how often this question comes up, even for seasoned developers. The answer isn’t just about syntax; it’s about the mental model you want for your data and how you want it to behave as it moves through your code.
-
Structs are value types: When you assign a struct to a new variable or pass it into a function, you get a brand new copy. Changes to one copy don’t affect the others. This is safe, predictable, and exactly what you want for simple data models, think coordinates, colors, or configurations. Swift leans heavily toward structs for this reason: they’re less likely to surprise you with hidden side effects.
-
Classes are reference types: Assign a class instance to a new variable, and both variables point to the same underlying object. Changes made through one reference are visible to all. This is powerful when you need shared, mutable state-like managing a single game controller or a network session. But it’s also a source of subtle bugs if you’re not careful.
When to Use a Struct
- Your data represents a simple value or a “thing” (like a point, rectangle, or user settings).
- You want copies to be independent, modifying one shouldn’t affect another.
- You don’t need inheritance or reference identity.
- You want the safety and performance benefits of value semantics (especially in multithreaded code).
When to Use a Class
- You need to model shared, mutable state, where multiple parts of your app should see changes to the same instance.
- Your type needs to inherit from another class (inheritance is only possible with classes).
- You need to use features like deinitializers (
deinit
) or reference identity checks.
Keep Note: “If I make a copy of this object and change something, should the original change too?” If the answer is no, reach for a struct. If the answer is yes, or if you need inheritance, classes are your tool.
If you’re just starting out, embrace the friction. The compiler’s warnings are not annoyances they’re signposts.
Did I make a mistake? Please consider Send Email With Subject