Swift Language & its features



This content originally appeared on DEV Community and was authored by Harsh Prajapat

Swift is a modern, high-performance programming language developed by Apple for building apps across its platforms — including iOS, macOS, watchOS, tvOS, and visionOS. It’s designed to be safe, fast, and expressive, making it a favorite among developers for both mobile and server-side development.

🚀 Key Features of Swift

  • Type Safety & Inference: Helps catch bugs early and reduces boilerplate code.
  • Optionals: Prevents null pointer crashes by safely handling missing values.
  • Closures: Similar to lambdas, enabling functional programming patterns.
  • Protocol-Oriented Programming: Encourages reusable and flexible code structures.
  • Memory Management: Uses Automatic Reference Counting (ARC) to manage memory efficiently.
  • Interoperability: Works seamlessly with Objective-C and C/C++ codebases.
  • Open Source: Available on Swift.org, with support for Linux, Windows, and Android in addition to Apple platforms.

🧪 Example: A Simple Swift Program

import Foundation

let greeting = "Hello, Swift!"
print(greeting)

This prints a friendly message to the console — simple, clean, and readable.

🌍 Where Swift Shines

Use Case Why Swift Works Well
iOS/macOS App Development Native performance, tight integration with Apple frameworks
Server-side Development Fast execution, safety, and open-source support
Embedded Systems Scales down to microcontrollers with minimal overhead
Cross-platform Tools Can be used with Swift Package Manager and third-party frameworks

Swift is constantly evolving — the latest versions include powerful features like concurrency support, macros, and data-race safety improvements.

Swift is packed with features that make it a powerful, safe, and expressive language for modern app development. Let’s explore what makes it stand out. 🚀

🧠 Core Features of Swift

  • Type Safety & Type Inference

    Prevents mismatched types and reduces boilerplate by automatically inferring types.

  • Optionals

    Helps handle the absence of a value safely, avoiding crashes from nil.

  • Closures

    Lightweight blocks of functionality that can be passed around and used in functional programming.

  • Protocols & Protocol-Oriented Programming

    Encourages flexible and reusable code by defining behavior contracts.

  • Generics

    Write flexible, reusable functions and types that work with any type.

  • Automatic Reference Counting (ARC)

    Efficient memory management without manual intervention.

  • Error Handling

    Built-in support for throwing, catching, and propagating errors using try, catch, and throws.

  • Tuples

    Group multiple values into a single compound value with mixed types.

  • Pattern Matching

    Powerful switch statements and if case constructs for clean control flow.

  • Value Types with Structs & Enums

    Safer and more predictable than reference types in many cases.

🧪 Modern Additions

  • Concurrency with async/await

    Simplifies asynchronous code and improves readability.

  • Actors

    Protect mutable state in concurrent environments.

  • Property Wrappers

    Encapsulate logic for property behaviors like @State, @Published, and @AppStorage.

  • Swift Macros (Swift 5.9+)

    Enable compile-time code generation for cleaner and more efficient code.

🛠 Developer-Friendly Features

  • Playgrounds

    Interactive coding environment for experimentation and learning.

  • Swift Package Manager (SPM)

    Built-in tool for managing dependencies and modular code.

  • Interoperability with Objective-C

    Seamlessly integrates with legacy codebases and Apple frameworks.

Swift is constantly evolving, and its blend of safety, performance, and expressiveness makes it ideal for everything from mobile apps to server-side development. Want to dive into any of these features with code examples or explore how Swift compares to other languages? I’m ready when you are. 💬💻

📚 Arrays

An array is an ordered collection of values of the same type. You can access elements by their index, starting from 0.

✅ Key Features:

  • Maintains order
  • Allows duplicates
  • Indexed access (array[0])

🧪 Example:

var fruits = ["Apple", "Banana", "Cherry"]
print(fruits[1]) // "Banana"

🔧 Common Operations:

  • append() – Add an item
  • insert(_:at:) – Insert at index
  • remove(at:) – Remove item
  • count – Number of elements
  • isEmpty – Check if empty

🧮 Sets

A set is an unordered collection of unique values. It’s great for checking membership or eliminating duplicates.

✅ Key Features:

  • No order
  • No duplicates
  • Fast membership checks

🧪 Example:

var colors: Set = ["Red", "Green", "Blue"]
colors.insert("Red") // Won’t add again
print(colors.contains("Green")) // true

🔧 Common Operations:

  • insert() – Add item
  • remove() – Remove item
  • union() – Combine sets
  • intersection() – Common items
  • subtracting() – Difference

🧷 Tuples

A tuple groups multiple values into a single compound value. Unlike arrays and sets, tuples can contain different types.

✅ Key Features:

  • Fixed size
  • Can hold mixed types
  • Useful for returning multiple values

🧪 Example:

let user = (name: "Alice", age: 30)
print(user.name) // "Alice"
print(user.age)  // 30

🔧 Usage Tips:

  • Access by index (user.0) or name (user.name)
  • Great for temporary groupings
  • Not ideal for large or dynamic data sets

🧠 Summary Table

Feature Array Set Tuple
Ordered ✅ ❌ ✅ (fixed order)
Duplicates ✅ ❌ ✅ (can repeat)
Mixed Types ❌ ❌ ✅
Index Access ✅ ❌ ✅
Use Case Lists Unique items Grouped values

In Swift, async and await are part of the structured concurrency model introduced in Swift 5.5. They make asynchronous code easier to write, read, and maintain — no more messy callback pyramids or completion handlers! 🚀

🧠 What Is async?

  • Marks a function as asynchronous, meaning it can pause and resume later.
  • Allows long-running tasks (like network calls) to run without blocking the main thread.
func fetchUserData() async -> String {
    // Simulate a delay
    return "User data loaded"
}

⏳ What Is await?

  • Tells Swift to wait for the result of an async function before continuing.
  • Must be used inside an async context.
func loadData() async {
    let result = await fetchUserData()
    print(result)
}

🧪 Example with Error Handling

func fetchImage() async throws -> Data {
    let url = URL(string: "https://example.com/image.png")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

Task {
    do {
        let imageData = try await fetchImage()
        print("Image size: \(imageData.count) bytes")
    } catch {
        print("Failed to fetch image: \(error)")
    }
}

🧵 Why Use async/await?

Benefit Description
✅ Cleaner Syntax No nested closures or callback hell
✅ Safer Error Handling Use try/catch with async code
✅ Better Performance Keeps UI responsive
✅ Easier to Read Code flows top-to-bottom like synchronous logic

Want to see how this compares to Combine or RxSwift? Or maybe how to convert old completion-based code to async/await? I can walk you through it! 🧭💬

In Swift, defer is a powerful keyword that lets you schedule code to be executed just before exiting the current scope — whether that’s a function, loop, or block. Think of it as a way to say, “Do this cleanup work last, no matter what.” 🧹

🧠 What defer Does

  • Executes its block when the surrounding scope ends
  • Runs even if the function exits early (via return, throw, etc.)
  • Helps with resource cleanup, like closing files or releasing locks

🧪 Example: File Handling

func writeLog() {
    let file = openFile()
    defer { closeFile(file) } // Always runs when function ends

    guard let data = fetchData() else { return }
    file.write(data)
}

Even if fetchData() fails and the function returns early, closeFile(file) will still be called.

🔁 Multiple defer Blocks

You can stack multiple defer statements. Swift executes them in reverse order — like unwinding a stack:

func testDeferOrder() {
    defer { print("First") }
    defer { print("Second") }
    defer { print("Third") }
    print("Done")
}

Output:

Done
Third
Second
First

🧩 Common Use Cases

  • Closing resources (files, network connections)
  • Unlocking locks in multithreaded code
  • Committing transactions (e.g., Core Animation or database)
  • Calling completion handlers in networking
  • Cleaning up temporary state in unit tests

⚠ Things to Watch Out For

  • You can’t break, continue, or return from inside a defer block
  • Avoid modifying return values inside defer — it’s unreliable for that purpose
  • defer doesn’t capture values at the time it’s declared — it uses the latest value when executed

In Swift, a lazy var is a stored property whose initial value isn’t calculated until it’s first accessed. It’s perfect for deferring expensive setup work until it’s actually needed. 💤⚡

🧠 Why Use lazy var?

  • Saves performance by delaying initialization
  • Useful when the property depends on external factors or complex setup
  • Only initialized once, and the result is cached

🧪 Example

class DataManager {
    lazy var data: [String] = {
        print("Loading data...")
        return ["Apple", "Banana", "Cherry"]
    }()
}

let manager = DataManager()
// At this point, `data` hasn't been initialized
print(manager.data) // Triggers initialization

Output:

Loading data...
["Apple", "Banana", "Cherry"]

🔍 Key Rules

  • Must be declared with varnot let
  • Can only be used with stored properties, not computed ones
  • Not thread-safe by default — be cautious in multithreaded contexts
  • Works only with classes and mutable structs

🧭 When to Use

Use Case lazy var is Ideal?
Expensive computation ✅ Yes
Conditional setup ✅ Yes
Always needed immediately ❌ No
Needs to update dynamically ❌ Use computed var

In Swift, access modifiers (also called access control levels) define the visibility and accessibility of your code entities — like classes, structs, properties, methods, and more. They help you encapsulate logic and protect internal implementation details. 🔐

🚦 Swift Access Levels Overview

Modifier Visibility Scope
open Accessible and subclassable outside the module. Most permissive.
public Accessible outside the module, but not subclassable externally.
internal Accessible within the same module. Default level.
fileprivate Accessible only within the same source file.
private Accessible only within the enclosing declaration and its extensions in file.

🧪 Example

public class Vehicle {
    internal var speed = 0
    private func startEngine() {
        print("Engine started")
    }
}
  • Vehicle can be accessed from other modules.
  • speed is accessible only within the same module.
  • startEngine() is hidden from everything except inside Vehicle.

🧠 Key Rules

  • You can’t expose a public entity that depends on a more restricted type.
  • open is only for classes and class members, allowing external subclassing and overriding.
  • internal is the default — no modifier needed.
  • Use fileprivate or private to hide implementation details.

🧵 When to Use What

Use Case Recommended Modifier
Public API for a framework open or public
Internal logic in your app internal
Helper methods in a file fileprivate
Temporary variables in a method private

🧠 Automatic Reference Counting (ARC)

ARC is Swift’s memory management system that automatically tracks and manages the memory used by class instances.

  • Every time you create a new class instance, ARC allocates memory for it.
  • ARC keeps a reference count: when the count drops to zero, the instance is deallocated.
  • ARC applies only to reference types (i.e., classes), not value types like structs or enums.

🔗 Strong References

  • The default type of reference in Swift.
  • A strong reference increases the reference count of an object.
  • As long as there’s at least one strong reference, the object stays in memory.
class Person {
    var name: String
    var pet: Dog? // strong reference by default
}

🧷 Weak References

  • A weak reference does not increase the reference count.
  • It’s declared with the weak keyword and must be optional (?) because it can become nil when the object is deallocated.
  • Used to avoid retain cycles when the referenced object might be deallocated independently.
class Dog {
    weak var owner: Person? // weak reference
}

🪢 Unowned References

  • Like weak references, unowned references don’t increase the reference count.
  • But they are non-optional and assume the referenced object will never be nil during their lifetime.
  • If accessed after deallocation, it causes a runtime crash.
class CreditCard {
    unowned let customer: Customer // unowned reference
}

🧩 Weak Self in Closures

  • Closures capture references, including self, which can lead to retain cycles.
  • Use [weak self] in the closure’s capture list to avoid this.
  • Since self becomes optional, you often need to unwrap it.
someAsyncCall { [weak self] in
    guard let self = self else { return }
    self.doSomething()
}

🔁 Retain Cycle

A retain cycle happens when two objects hold strong references to each other, preventing ARC from deallocating them.

Example:

class Person {
    var apartment: Apartment?
}

class Apartment {
    var tenant: Person?
}

If both apartment and tenant are strong references, neither object will be deallocated. To fix this, make one of them weak or unowned.

Grand Central Dispatch (GCD) is Apple’s low-level concurrency framework that helps you execute tasks asynchronously and concurrently, making your iOS apps more responsive and efficient. 🚀

🧠 What Is GCD?

GCD manages dispatch queues, which are thread-safe structures that hold tasks (blocks of code) to be executed. It abstracts away the complexity of thread management, letting you focus on what needs to be done — not how it’s scheduled.

🧵 Types of Dispatch Queues

Queue Type Description
Main Queue Serial queue tied to the main thread — used for UI updates
Global Queues Concurrent queues provided by the system with different QoS levels
Custom Queues You can create your own serial or concurrent queues
let serialQueue = DispatchQueue(label: "com.myapp.serial")
let concurrentQueue = DispatchQueue(label: "com.myapp.concurrent", attributes: .concurrent)

⚙ Synchronous vs Asynchronous Execution

  • sync: Blocks the current thread until the task finishes.
  • async: Executes the task in the background and returns immediately.
DispatchQueue.global().async {
    // Background task
    DispatchQueue.main.async {
        // UI update
    }
}

🎯 Quality of Service (QoS)

GCD lets you prioritize tasks using QoS classes:

QoS Class Use Case
.userInteractive Immediate UI updates
.userInitiated Tasks triggered by user actions
.utility Long-running tasks
.background Non-urgent tasks like backups

🧩 Advanced Features

  • DispatchGroup: Wait for multiple tasks to finish
  • DispatchSemaphore: Control access to resources
  • DispatchWorkItem: Encapsulate work with cancelation and dependencies
  • Barrier Tasks: Synchronize access in concurrent queues

GCD is the backbone of concurrency in iOS, and mastering it unlocks smoother animations, faster data processing, and better user experiences. Want to dive into DispatchGroup or see how GCD compares to Combine or async/await? I’ve got examples ready to go. 🧪📱

DispatchGroup in iOS is a powerful tool from Grand Central Dispatch (GCD) that lets you manage multiple asynchronous tasks and get notified when they’ve all completed. It’s perfect for coordinating parallel operations like network requests, image processing, or database queries. 🧠🔄

🧩 What Is DispatchGroup?

A DispatchGroup lets you:

  • Enter the group when a task starts
  • Leave the group when a task finishes
  • Notify when all tasks are done

This helps you synchronize multiple async operations and trigger a final action once everything is complete.

🛠 Basic Usage Example

let group = DispatchGroup()

group.enter()
fetchUserData { result in
    // handle result
    group.leave()
}

group.enter()
fetchPosts { result in
    // handle result
    group.leave()
}

group.notify(queue: .main) {
    print("All tasks completed! Update UI here.")
}

Each enter() signals that a task has started, and leave() signals it’s done. Once all entered tasks have left, the notify block runs.

🧪 Real-World Use Case

Imagine loading a profile screen that needs:

  • User info
  • Profile picture
  • Recent posts

You can fire all three requests in parallel using DispatchGroup, and update the UI only when all are done — improving performance and user experience.

🧠 Key Points

Method Purpose
enter() Manually signal a task has started
leave() Signal task completion
notify(queue:) Run code after all tasks finish
wait() Block current thread until tasks finish (use cautiously!)


This content originally appeared on DEV Community and was authored by Harsh Prajapat